diff options
author | Winson Chung <winsonc@google.com> | 2014-03-07 15:06:19 -0800 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2014-03-11 12:03:08 -0700 |
commit | 303e1ff1fec8b240b587bb18b981247a99833aa8 (patch) | |
tree | 277b80ed79659b89d26ce701af9d60593f3b2d9d /packages | |
parent | 4cfde32ff0105d7c8a4743e084f1c9775f4c6b5c (diff) | |
download | frameworks_base-303e1ff1fec8b240b587bb18b981247a99833aa8.zip frameworks_base-303e1ff1fec8b240b587bb18b981247a99833aa8.tar.gz frameworks_base-303e1ff1fec8b240b587bb18b981247a99833aa8.tar.bz2 |
Initial changes for recents.
Change-Id: Ide2c202b4a5b25410f0f32bd0a81ccf817ede38f
Diffstat (limited to 'packages')
28 files changed, 4158 insertions, 18 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 8d6fe41..b09cc1d 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -52,6 +52,7 @@ <uses-permission android:name="android.permission.START_ANY_ACTIVITY" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.GET_TOP_ACTIVITY_INFO" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /> <!-- WindowManager --> <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" /> @@ -140,6 +141,18 @@ </intent-filter> </receiver> + <!-- Alternate Recents --> + <activity android:name=".recents.RecentsActivity" + android:launchMode="singleInstance" + android:excludeFromRecents="true" + android:theme="@style/RecentsTheme"> + <intent-filter> + <action android:name="com.android.systemui.recents.TOGGLE_RECENTS" /> + </intent-filter> + </activity> + + <service android:name=".recents.RecentsService" /> + <!-- started from UsbDeviceSettingsManager --> <activity android:name=".usb.UsbConfirmActivity" android:exported="true" diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index 1ff93d2..48d9722 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -6,6 +6,11 @@ public void setGlowAlpha(float); public void setGlowScale(float); } +-keep class com.android.systemui.recents.views.TaskIconView { + public void setCircularClipRadius(float); + public float getCircularClipRadius(); +} -keep class com.android.systemui.statusbar.phone.PhoneStatusBar -keep class com.android.systemui.statusbar.tv.TvStatusBar +-keep class com.android.systemui.recents.*
\ No newline at end of file diff --git a/packages/SystemUI/res/anim/recents_from_launcher_enter.xml b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml new file mode 100644 index 0000000..4bd7e82 --- /dev/null +++ b/packages/SystemUI/res/anim/recents_from_launcher_enter.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2012, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false" + android:zAdjustment="top"> + <alpha android:fromAlpha="0.0" android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" android:fillAfter="true" + android:interpolator="@android:interpolator/accelerate_cubic" + android:duration="250"/> +</set> diff --git a/packages/SystemUI/res/anim/recents_from_launcher_exit.xml b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml new file mode 100644 index 0000000..becc9d0 --- /dev/null +++ b/packages/SystemUI/res/anim/recents_from_launcher_exit.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2012, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false" + android:zAdjustment="normal"> + <alpha android:fromAlpha="1.0" android:toAlpha="0.0" + android:fillEnabled="true" + android:fillBefore="true" android:fillAfter="true" + android:interpolator="@android:interpolator/decelerate_cubic" + android:duration="250"/> +</set> diff --git a/packages/SystemUI/res/layout/recents_empty.xml b/packages/SystemUI/res/layout/recents_empty.xml new file mode 100644 index 0000000..6268628 --- /dev/null +++ b/packages/SystemUI/res/layout/recents_empty.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textSize="40sp" + android:textColor="#ffffffff" + android:text="@string/recents_empty_message" + android:fontFamily="sans-serif-thin" + android:visibility="gone" />
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 94796af..ce05639 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -507,6 +507,9 @@ <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] --> <string name="quick_settings_color_space_label">Color correction mode</string> + <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] --> + <string name="recents_empty_message">RECENTS</string> + <!-- Glyph to be overlaid atop the battery when the level is extremely low. Do not translate. --> <string name="battery_meter_very_low_overlay_symbol">!</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 54f03bd..14af020 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -20,6 +20,13 @@ <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item> </style> + <!-- Alternate Recents theme --> + <style name="RecentsTheme" parent="@android:style/Theme.Holo.Wallpaper.NoTitleBar"> + <item name="android:windowTranslucentStatus">true</item> + <item name="android:windowTranslucentNavigation">true</item> + <item name="android:windowAnimationStyle">@style/Animation.RecentsActivity</item> + </style> + <!-- Animations for a non-full-screen window or activity. --> <style name="Animation.RecentsActivity" parent="@android:style/Animation.Activity"> <item name="android:activityOpenEnterAnimation">@anim/recents_launch_from_launcher_enter</item> diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java index f5670e1..07c0c78 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java @@ -16,37 +16,143 @@ package com.android.systemui.recent; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; import android.view.View; - +import android.view.WindowManager; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SystemUI; +import java.util.List; + + public class Recents extends SystemUI implements RecentsComponent { + /** A handler for messages from the recents implementation */ + class RecentsMessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + if (!mUseAlternateRecents) return; + if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) { + Resources res = mContext.getResources(); + float statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + mFirstTaskRect = (Rect) msg.getData().getParcelable("taskRect"); + mFirstTaskRect.offset(0, (int) statusBarHeight); + } + } + } + + /** A service connection to the recents implementation */ + class RecentsServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceConnected] toggleRecents: " + + mToggleRecentsUponServiceBound); + mService = new Messenger(service); + mServiceIsBound = true; + + // Toggle recents if this service connection was triggered by hitting the recents button + if (mToggleRecentsUponServiceBound) { + startAlternateRecentsActivity(); + } + mToggleRecentsUponServiceBound = false; + } + + @Override + public void onServiceDisconnected(ComponentName className) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceDisconnected]"); + mService = null; + mServiceIsBound = false; + } + } + private static final String TAG = "Recents"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; + + final static int MSG_UPDATE_FOR_CONFIGURATION = 0; + final static int MSG_UPDATE_TASK_THUMBNAIL = 1; + final static int MSG_PRELOAD_TASKS = 2; + final static int MSG_CANCEL_PRELOAD_TASKS = 3; + + final static String sToggleRecentsAction = "com.android.systemui.recents.TOGGLE_RECENTS"; + final static String sRecentsPackage = "com.android.systemui"; + final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; + final static String sRecentsService = "com.android.systemui.recents.RecentsService"; + + // Which recents to use + boolean mUseAlternateRecents; + + // Recents service binding + Messenger mService = null; + Messenger mMessenger; + boolean mServiceIsBound = false; + boolean mToggleRecentsUponServiceBound; + RecentsServiceConnection mConnection = new RecentsServiceConnection(); + + View mStatusBarView; + Rect mFirstTaskRect = new Rect(); + + public Recents() { + mMessenger = new Messenger(new RecentsMessageHandler()); + } @Override public void start() { + mUseAlternateRecents = + SystemProperties.getBoolean("persist.recents.use_alternate", false); + putComponent(RecentsComponent.class, this); + + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|start]"); + + // Try to create a long-running connection to the recents service + bindToRecentsService(false); + } } @Override public void toggleRecents(Display display, int layoutDirection, View statusBarView) { + if (mUseAlternateRecents) { + // Launch the alternate recents if required + toggleAlternateRecents(display, layoutDirection, statusBarView); + return; + } + if (DEBUG) Log.d(TAG, "toggle recents panel"); try { TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask(); @@ -190,33 +296,227 @@ public class Recents extends SystemUI implements RecentsComponent { } } + /** Toggles the alternate recents activity */ + public void toggleAlternateRecents(Display display, int layoutDirection, View statusBarView) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|toggleRecents] serviceIsBound: " + mServiceIsBound); + mStatusBarView = statusBarView; + if (!mServiceIsBound) { + // Try to create a long-running connection to the recents service before toggling + // recents + bindToRecentsService(true); + return; + } + + try { + startAlternateRecentsActivity(); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to launch RecentAppsIntent", e); + } + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (mServiceIsBound) { + Resources res = mContext.getResources(); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + int navBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height); + Rect rect = new Rect(); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRectSize(rect); + + // Try and update the recents configuration + try { + Bundle data = new Bundle(); + data.putParcelable("windowRect", rect); + data.putParcelable("systemInsets", new Rect(0, statusBarHeight, 0, 0)); + Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0); + msg.setData(data); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + + /** Binds to the recents implementation */ + private void bindToRecentsService(boolean toggleRecentsUponConnection) { + if (!mUseAlternateRecents) return; + + mToggleRecentsUponServiceBound = toggleRecentsUponConnection; + Intent intent = new Intent(); + intent.setClassName(sRecentsPackage, sRecentsService); + mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + /** Loads the first task thumbnail */ + Bitmap loadFirstTaskThumbnail() { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + for (ActivityManager.RecentTaskInfo t : tasks) { + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + return null; + } + + Bitmap thumbnail = am.getTaskTopThumbnail(t.persistentId); + return thumbnail; + } + return null; + } + + /** Returns whether there is a first task */ + boolean hasFirstTask() { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + for (ActivityManager.RecentTaskInfo t : tasks) { + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + continue; + } + + return true; + } + return false; + } + + /** Converts from the device rotation to the degree */ + float getDegreesForRotation(int value) { + switch (value) { + case Surface.ROTATION_90: + return 360f - 90f; + case Surface.ROTATION_180: + return 360f - 180f; + case Surface.ROTATION_270: + return 360f - 270f; + } + return 0f; + } + + /** Takes a screenshot of the surface */ + Bitmap takeScreenshot(Display display) { + DisplayMetrics dm = new DisplayMetrics(); + display.getRealMetrics(dm); + float[] dims = {dm.widthPixels, dm.heightPixels}; + float degrees = getDegreesForRotation(display.getRotation()); + boolean requiresRotation = (degrees > 0); + if (requiresRotation) { + // Get the dimensions of the device in its native orientation + Matrix m = new Matrix(); + m.preRotate(-degrees); + m.mapPoints(dims); + dims[0] = Math.abs(dims[0]); + dims[1] = Math.abs(dims[1]); + } + return SurfaceControl.screenshot((int) dims[0], (int) dims[1]); + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity() { + Rect taskRect = mFirstTaskRect; + if (taskRect != null && taskRect.width() > 0 && taskRect.height() > 0 && hasFirstTask()) { + // Loading from thumbnail + Bitmap thumbnail; + Bitmap firstThumbnail = loadFirstTaskThumbnail(); + if (firstThumbnail == null) { + // Load the thumbnail from the screenshot + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Bitmap screenshot = takeScreenshot(display); + Resources res = mContext.getResources(); + int size = Math.min(screenshot.getWidth(), screenshot.getHeight()); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size), + new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null); + c.setBitmap(null); + // Recycle the old screenshot + screenshot.recycle(); + } else { + // Create the thumbnail + thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(), + Bitmap.Config.ARGB_8888); + int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), + new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null); + c.setBitmap(null); + // Recycle the old thumbnail + firstThumbnail.recycle(); + } + + ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, + thumbnail, mFirstTaskRect.left, mFirstTaskRect.top, null); + startAlternateRecentsActivity(opts); + } else { + ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.recents_from_launcher_enter, + R.anim.recents_from_launcher_exit); + startAlternateRecentsActivity(opts); + } + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity(ActivityOptions opts) { + Intent intent = new Intent(sToggleRecentsAction); + intent.setClassName(sRecentsPackage, sRecentsActivity); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + if (opts != null) { + mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( + UserHandle.USER_CURRENT)); + } else { + mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + } + } + @Override public void preloadRecentTasksList() { - if (DEBUG) Log.d(TAG, "preloading recents"); - Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|preloadRecents]"); + } else { + Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); - RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + } } @Override public void cancelPreloadingRecentTasksList() { - if (DEBUG) Log.d(TAG, "cancel preloading recents"); - Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|cancelPreload]"); + } else { + Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); - RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } } @Override public void closeRecents() { - if (DEBUG) Log.d(TAG, "closing recents panel"); - Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); - intent.setPackage("com.android.systemui"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|closeRecents]"); + } else { + Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); + intent.setPackage("com.android.systemui"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/Console.java b/packages/SystemUI/src/com/android/systemui/recents/Console.java new file mode 100644 index 0000000..b3d9ccf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Console.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + + +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.Toast; + +public class Console { + // Colors + public static final String AnsiReset = "\u001B[0m"; + public static final String AnsiBlack = "\u001B[30m"; + public static final String AnsiRed = "\u001B[31m"; // SystemUIHandshake + public static final String AnsiGreen = "\u001B[32m"; // MeasureAndLayout + public static final String AnsiYellow = "\u001B[33m"; // SynchronizeViewsWithModel + public static final String AnsiBlue = "\u001B[34m"; // TouchEvents + public static final String AnsiPurple = "\u001B[35m"; // Draw + public static final String AnsiCyan = "\u001B[36m"; // ClickEvents + public static final String AnsiWhite = "\u001B[37m"; + + /** Logs a key */ + public static void log(String key) { + Console.log(true, key, "", AnsiReset); + } + + /** Logs a conditioned key */ + public static void log(boolean condition, String key) { + if (condition) { + Console.log(condition, key, "", AnsiReset); + } + } + + /** Logs a key in a specific color */ + public static void log(boolean condition, String key, Object data) { + if (condition) { + Console.log(condition, key, data, AnsiReset); + } + } + + /** Logs a key with data in a specific color */ + public static void log(boolean condition, String key, Object data, String color) { + if (condition) { + System.out.println(color + key + AnsiReset + " " + data.toString()); + } + } + + /** Logs an error */ + public static void logError(Context context, String msg) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + Log.e("Recents", msg); + } + + /** Logs a divider bar */ + public static void logDivider(boolean condition) { + if (condition) { + System.out.println("==== [" + System.currentTimeMillis() + + "] ============================================================"); + } + } + + /** Returns the stringified MotionEvent action */ + public static String motionEventActionToString(int action) { + switch (action) { + case MotionEvent.ACTION_DOWN: + return "Down"; + case MotionEvent.ACTION_UP: + return "Up"; + case MotionEvent.ACTION_MOVE: + return "Move"; + case MotionEvent.ACTION_CANCEL: + return "Cancel"; + case MotionEvent.ACTION_POINTER_DOWN: + return "Pointer Down"; + case MotionEvent.ACTION_POINTER_UP: + return "Pointer Up"; + default: + return "" + action; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java new file mode 100644 index 0000000..aeae4ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +/** + * Constants + * XXX: We are going to move almost all of these into a resource. + */ +public class Constants { + public static class DebugFlags { + // Enable this with any other debug flag to see more info + public static final boolean Verbose = false; + + public static class App { + public static final boolean EnableTaskFiltering = false; + public static final boolean EnableTaskStackClipping = false; + public static final boolean EnableBackgroundTaskLoading = true; + public static final boolean ForceDisableBackgroundCache = false; + public static final boolean TaskDataLoader = false; + public static final boolean SystemUIHandshake = false; + public static final boolean TimeSystemCalls = false; + } + + public static class UI { + public static final boolean Draw = false; + public static final boolean ClickEvents = false; + public static final boolean TouchEvents = false; + public static final boolean MeasureAndLayout = false; + public static final boolean Clipping = false; + public static final boolean HwLayers = true; + } + + public static class TaskStack { + public static final boolean SynchronizeViewsWithModel = false; + } + + public static class ViewPool { + public static final boolean PoolCallbacks = false; + } + } + + public static class Values { + public static class Window { + public static final float DarkBackgroundDim = 0.5f; + public static final float BackgroundDim = 0.35f; + } + + public static class RecentsTaskLoader { + // XXX: This should be calculated on the first load + public static final int PreloadFirstTasksCount = 5; + public static final int TaskEntryMultiplier = 1; + } + + public static class TaskStackView { + public static class Animation { + public static final int TaskRemovedReshuffleDuration = 200; + public static final int SnapScrollBackDuration = 650; + public static final int SwipeDismissDuration = 350; + public static final int SwipeSnapBackDuration = 350; + } + + // The padding will be applied to the smallest dimension, and then applied to all sides + public static final float StackPaddingPct = 0.15f; + // The overlap height relative to the task height + public static final float StackOverlapPct = 0.65f; + // The height of the peek space relative to the stack height + public static final float StackPeekHeightPct = 0.1f; + // The min scale of the last card in the peek area + public static final float StackPeekMinScale = 0.9f; + // The number of cards we see in the peek space + public static final int StackPeekNumCards = 3; + } + + public static class TaskView { + public static class Animation { + public static final int TaskDataUpdatedFadeDuration = 250; + public static final int TaskIconCircularClipInDuration = 225; + public static final int TaskIconCircularClipOutDuration = 85; + } + + public static final boolean AnimateFrontTaskIconOnEnterRecents = true; + public static final boolean AnimateFrontTaskIconOnLeavingRecents = true; + public static final boolean AnimateFrontTaskIconOnLeavingUseClip = false; + public static final boolean DrawColoredTaskBars = false; + public static final boolean UseRoundedCorners = true; + public static final float RoundedCornerRadiusDps = 3; + + public static final float TaskBarHeightDps = 54; + public static final float TaskIconSizeDps = 60; + } + } + + // UNMIGRATED CONSTANTS: + + /** Determines whether to layout the stack vertically in landscape mode */ + public static final boolean LANDSCAPE_LAYOUT_VERTICAL_STACK = true; +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java new file mode 100644 index 0000000..d050847 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.RecentsView; +import com.android.systemui.R; + +import java.util.ArrayList; + + +/* Activity */ +public class RecentsActivity extends Activity { + FrameLayout mContainerView; + RecentsView mRecentsView; + View mEmptyView; + boolean mVisible; + + /** Updates the set of recent tasks */ + void updateRecentsTasks() { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SpaceNode root = loader.reload(this, Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount); + ArrayList<TaskStack> stacks = root.getStacks(); + if (!stacks.isEmpty()) { + // XXX: We just replace the root every time for now, we will change this in the future + mRecentsView.setBSP(root); + } + + // Add the default no-recents layout + if (stacks.size() == 1 && stacks.get(0).getTaskCount() == 0) { + mEmptyView.setVisibility(View.VISIBLE); + + // Dim the background even more + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = Constants.Values.Window.DarkBackgroundDim; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } else { + mEmptyView.setVisibility(View.GONE); + } + } + + /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ + boolean dismissRecentsIfVisible(Intent intent) { + if ("com.android.systemui.recents.TOGGLE_RECENTS".equals(intent.getAction())) { + if (mVisible) { + if (!mRecentsView.launchFirstTask()) { + finish(); + } + return true; + } + } + return false; + } + + /** Called with the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onCreate]", + getIntent().getAction() + " visible: " + mVisible, Console.AnsiRed); + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Dismiss recents if it is visible and we are toggling + if (dismissRecentsIfVisible(getIntent())) return; + + // Set the background dim + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = Constants.Values.Window.BackgroundDim; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + + // Create the view hierarchy + mRecentsView = new RecentsView(this); + mRecentsView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // Create the empty view + LayoutInflater inflater = LayoutInflater.from(this); + mEmptyView = inflater.inflate(R.layout.recents_empty, mContainerView, false); + + mContainerView = new FrameLayout(this); + mContainerView.addView(mRecentsView); + mContainerView.addView(mEmptyView); + setContentView(mContainerView); + + // Update the recent tasks + updateRecentsTasks(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onNewIntent]", + intent.getAction() + " visible: " + mVisible, Console.AnsiRed); + + // Dismiss recents if it is visible and we are toggling + if (dismissRecentsIfVisible(intent)) return; + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Update the recent tasks + updateRecentsTasks(); + } + + @Override + protected void onStart() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStart]", "", + Console.AnsiRed); + super.onStart(); + mVisible = true; + } + + @Override + protected void onResume() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onResume]", "", + Console.AnsiRed); + super.onResume(); + } + + @Override + protected void onPause() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onPause]", "", + Console.AnsiRed); + super.onPause(); + + // Stop the loader when we leave Recents + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.stopLoader(); + } + + @Override + protected void onStop() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStop]", "", + Console.AnsiRed); + super.onStop(); + mVisible = false; + } + + @Override + public void onBackPressed() { + if (!mRecentsView.unfilterFilteredStacks()) { + super.onBackPressed(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java new file mode 100644 index 0000000..f3881ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.util.TypedValue; + + +/** A static Recents configuration for the current context + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsConfiguration { + static RecentsConfiguration sInstance; + + DisplayMetrics mDisplayMetrics; + + public boolean layoutVerticalStack; + public Rect systemInsets = new Rect(); + + /** Private constructor */ + private RecentsConfiguration() {} + + /** Updates the configuration to the current context */ + public static RecentsConfiguration reinitialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsConfiguration(); + } + sInstance.update(context); + return sInstance; + } + + /** Returns the current recents configuration */ + public static RecentsConfiguration getInstance() { + return sInstance; + } + + /** Updates the state, given the specified context */ + void update(Context context) { + mDisplayMetrics = context.getResources().getDisplayMetrics(); + + boolean isPortrait = context.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_PORTRAIT; + layoutVerticalStack = isPortrait || Constants.LANDSCAPE_LAYOUT_VERTICAL_STACK; + } + + public void updateSystemInsets(Rect insets) { + systemInsets.set(insets); + } + + /** Converts from DPs to PXs */ + public int pxFromDp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, mDisplayMetrics)); + } + /** Converts from SPs to PXs */ + public int pxFromSp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, mDisplayMetrics)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java new file mode 100644 index 0000000..522ab0f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.TaskStackView; +import com.android.systemui.recents.views.TaskViewTransform; + + +/* Service */ +public class RecentsService extends Service { + // XXX: This should be getting the message from recents definition + final static int MSG_UPDATE_RECENTS_FOR_CONFIGURATION = 0; + + class MessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|handleMessage]", msg); + if (msg.what == MSG_UPDATE_RECENTS_FOR_CONFIGURATION) { + Context context = RecentsService.this; + RecentsTaskLoader.initialize(context); + RecentsConfiguration.reinitialize(context); + + try { + Bundle data = msg.getData(); + Rect windowRect = (Rect) data.getParcelable("windowRect"); + Rect systemInsets = (Rect) data.getParcelable("systemInsets"); + RecentsConfiguration.getInstance().updateSystemInsets(systemInsets); + + // Create a dummy task stack & compute the rect for the thumbnail to animate to + TaskStack stack = new TaskStack(context); + TaskStackView tsv = new TaskStackView(context, stack); + tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top); + tsv.boundScroll(); + TaskViewTransform transform = tsv.getStackTransform(0); + + data.putParcelable("taskRect", transform.rect); + Message reply = Message.obtain(null, MSG_UPDATE_RECENTS_FOR_CONFIGURATION, 0, 0); + reply.setData(data); + msg.replyTo.send(reply); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + } + + Messenger mMessenger = new Messenger(new MessageHandler()); + + @Override + public void onCreate() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onCreate]"); + super.onCreate(); + } + + @Override + public IBinder onBind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onBind]"); + return mMessenger.getBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onUnbind]"); + return super.onUnbind(intent); + } + + @Override + public void onRebind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onRebind]"); + super.onRebind(intent); + } + + @Override + public void onDestroy() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onDestroy]"); + super.onDestroy(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java new file mode 100644 index 0000000..c303ca7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.UserHandle; +import android.util.LruCache; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** A bitmap load queue */ +class TaskResourceLoadQueue { + ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); + + Task nextTask() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|nextTask]"); + return mQueue.poll(); + } + + void addTask(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|addTask]"); + if (!mQueue.contains(t)) { + mQueue.add(t); + } + synchronized(this) { + notifyAll(); + } + } + + void removeTask(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|removeTask]"); + mQueue.remove(t); + } + + void clearTasks() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|clearTasks]"); + mQueue.clear(); + } + + boolean isEmpty() { + return mQueue.isEmpty(); + } +} + +/* Task resource loader */ +class TaskResourceLoader implements Runnable { + Context mContext; + HandlerThread mLoadThread; + Handler mLoadThreadHandler; + Handler mMainThreadHandler; + + TaskResourceLoadQueue mLoadQueue; + DrawableLruCache mIconCache; + BitmapLruCache mThumbnailCache; + boolean mCancelled; + + /** Constructor, creates a new loading thread that loads task resources in the background */ + public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache iconCache, + BitmapLruCache thumbnailCache) { + mLoadQueue = loadQueue; + mIconCache = iconCache; + mThumbnailCache = thumbnailCache; + mMainThreadHandler = new Handler(); + mLoadThread = new HandlerThread("Recents-TaskResourceLoader"); + mLoadThread.setPriority(Thread.NORM_PRIORITY - 1); + mLoadThread.start(); + mLoadThreadHandler = new Handler(mLoadThread.getLooper()); + mLoadThreadHandler.post(this); + } + + /** Restarts the loader thread */ + void start(Context context) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|start]"); + mContext = context; + mCancelled = false; + // Notify the load thread to start loading + synchronized(mLoadThread) { + mLoadThread.notifyAll(); + } + } + + /** Requests the loader thread to stop after the current iteration */ + void stop() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|stop]"); + // Mark as cancelled for the thread to pick up + mCancelled = true; + } + + @Override + public void run() { + while (true) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|run|" + Thread.currentThread().getId() + "]"); + if (mCancelled) { + // We have to unset the context here, since the background thread may be using it + // when we call stop() + mContext = null; + // If we are cancelled, then wait until we are started again + synchronized(mLoadThread) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadThreadCancelled]"); + mLoadThread.wait(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } else { + // Load the next item from the queue + final Task t = mLoadQueue.nextTask(); + if (t != null) { + try { + Drawable cachedIcon = mIconCache.get(t); + Bitmap cachedThumbnail = mThumbnailCache.get(t); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|load]", + t + " icon: " + cachedIcon + " thumbnail: " + cachedThumbnail); + // Load the icon + if (cachedIcon == null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(t.intent.getComponent(), + PackageManager.GET_META_DATA); + Drawable icon = info.loadIcon(pm); + if (!mCancelled) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadIcon]", + icon); + t.icon = icon; + mIconCache.put(t, icon); + } + } + // Load the thumbnail + if (cachedThumbnail == null) { + ActivityManager am = (ActivityManager) + mContext.getSystemService(Context.ACTIVITY_SERVICE); + Bitmap thumbnail = am.getTaskTopThumbnail(t.id); + if (!mCancelled) { + if (thumbnail != null) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadThumbnail]", + thumbnail); + t.thumbnail = thumbnail; + mThumbnailCache.put(t, thumbnail); + } else { + Console.logError(mContext, + "Failed to load task top thumbnail for: " + + t.intent.getComponent().getPackageName()); + } + } + } + if (!mCancelled) { + // Notify that the task data has changed + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + t.notifyTaskDataChanged(); + } + }); + } + } catch (PackageManager.NameNotFoundException ne) { + ne.printStackTrace(); + } + } + + // If there are no other items in the list, then just wait until something is added + if (!mCancelled && mLoadQueue.isEmpty()) { + synchronized(mLoadQueue) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadQueue]"); + mLoadQueue.wait(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + } +} + +/** The drawable cache */ +class DrawableLruCache extends LruCache<Task, Drawable> { + public DrawableLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task t, Drawable d) { + // The cache size will be measured in kilobytes rather than number of items + // NOTE: this isn't actually correct, as the icon may be smaller + int maxBytes = (d.getIntrinsicWidth() * d.getIntrinsicHeight() * 4); + return maxBytes / 1024; + } +} + +/** The bitmap cache */ +class BitmapLruCache extends LruCache<Task, Bitmap> { + public BitmapLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task t, Bitmap bitmap) { + // The cache size will be measured in kilobytes rather than number of items + return bitmap.getByteCount() / 1024; + } +} + +/* Recents task loader + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsTaskLoader { + static RecentsTaskLoader sInstance; + + DrawableLruCache mIconCache; + BitmapLruCache mThumbnailCache; + TaskResourceLoadQueue mLoadQueue; + TaskResourceLoader mLoader; + + BitmapDrawable mDefaultIcon; + Bitmap mDefaultThumbnail; + + /** Private Constructor */ + private RecentsTaskLoader(Context context) { + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int iconCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 16; + int thumbnailCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 8; + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize + + " iconCache: " + iconCacheSize); + mLoadQueue = new TaskResourceLoadQueue(); + mIconCache = new DrawableLruCache(iconCacheSize); + mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); + mLoader = new TaskResourceLoader(mLoadQueue, mIconCache, mThumbnailCache); + + // Create the default assets + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(); + c.setBitmap(icon); + c.drawColor(0x00000000); + c.setBitmap(mDefaultThumbnail); + c.drawColor(0x00000000); + c.setBitmap(null); + mDefaultIcon = new BitmapDrawable(context.getResources(), icon); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|defaultBitmaps]", + "icon: " + mDefaultIcon + " thumbnail: " + mDefaultThumbnail, Console.AnsiRed); + } + + /** Initializes the recents task loader */ + public static RecentsTaskLoader initialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsTaskLoader(context); + } + return sInstance; + } + + /** Returns the current recents task loader */ + public static RecentsTaskLoader getInstance() { + return sInstance; + } + + /** Reload the set of recent tasks */ + SpaceNode reload(Context context, int preloadCount) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|reload]"); + TaskStack stack = new TaskStack(context); + SpaceNode root = new SpaceNode(context); + root.setStack(stack); + try { + long t1 = System.currentTimeMillis(); + + PackageManager pm = context.getPackageManager(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + // Get the recent tasks + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(25, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + Collections.reverse(tasks); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getRecentTasks]", + "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|tasks]", "" + tasks.size()); + + // Remove home/recents tasks + Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); + while (iter.hasNext()) { + ActivityManager.RecentTaskInfo t = iter.next(); + + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + iter.remove(); + continue; + } + // Skip tasks from this Recents package + if (t.baseIntent.getComponent().getPackageName().equals(context.getPackageName())) { + iter.remove(); + continue; + } + } + + // Add each task to the task stack + t1 = System.currentTimeMillis(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = tasks.get(i); + + // Load the label, icon and thumbnail + ActivityInfo info = pm.getActivityInfo(t.baseIntent.getComponent(), + PackageManager.GET_META_DATA); + String title = info.loadLabel(pm).toString(); + Drawable icon = null; + Bitmap thumbnail = null; + Task task; + if (i >= (taskCount - preloadCount) || !Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|preloadTask]", + "i: " + i + " task: " + t.baseIntent.getComponent().getPackageName()); + icon = info.loadIcon(pm); + thumbnail = am.getTaskTopThumbnail(t.id); + for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); + task = new Task(t.persistentId, t.baseIntent, title, icon, thumbnail); + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + if (thumbnail != null) mThumbnailCache.put(task, thumbnail); + if (icon != null) { + mIconCache.put(task, icon); + } + } + stack.addTask(task); + } + } else { + for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); + task = new Task(t.persistentId, t.baseIntent, title, null, null); + stack.addTask(task); + } + } + + /* + if (stacks.containsKey(t.stackId)) { + builder = stacks.get(t.stackId); + } else { + builder = new TaskStackBuilder(); + stacks.put(t.stackId, builder); + } + */ + } + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getAllTaskTopThumbnail]", + "" + (System.currentTimeMillis() - t1) + "ms"); + + /* + // Get all the stacks + t1 = System.currentTimeMillis(); + List<ActivityManager.StackInfo> stackInfos = ams.getAllStackInfos(); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllStackInfos]", "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|stacks]", "" + tasks.size()); + for (ActivityManager.StackInfo s : stackInfos) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, " [RecentsTaskLoader|stack]", s.toString()); + if (stacks.containsKey(s.stackId)) { + stacks.get(s.stackId).setRect(s.bounds); + } + } + */ + } catch (Exception e) { + e.printStackTrace(); + } + mLoader.start(context); + return root; + } + + /** Acquires the task resource data from the pool. + * XXX: Move this into Task? */ + public void loadTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + t.icon = mIconCache.get(t); + t.thumbnail = mThumbnailCache.get(t); + + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]", + t + " icon: " + t.icon + " thumbnail: " + t.thumbnail); + + boolean requiresLoad = false; + if (t.icon == null) { + t.icon = mDefaultIcon; + requiresLoad = true; + } + if (t.thumbnail == null) { + t.thumbnail = mDefaultThumbnail; + requiresLoad = true; + } + if (requiresLoad) { + mLoadQueue.addTask(t); + } + } + } + + /** Releases the task resource data back into the pool. + * XXX: Move this into Task? */ + public void unloadTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|unloadTask]", t); + mLoadQueue.removeTask(t); + t.icon = mDefaultIcon; + t.thumbnail = mDefaultThumbnail; + } + } + + /** Completely removes the resource data from the pool. + * XXX: Move this into Task? */ + public void deleteTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|deleteTask]", t); + mLoadQueue.removeTask(t); + mThumbnailCache.remove(t); + mIconCache.remove(t); + } + t.icon = mDefaultIcon; + t.thumbnail = mDefaultThumbnail; + } + + /** Stops the task loader */ + void stopLoader() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stopLoader]"); + mLoader.stop(); + mLoadQueue.clearTasks(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java new file mode 100644 index 0000000..33e4246 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents; + +import android.graphics.Rect; + +/* Common code */ +public class Utilities { + public static final Rect tmpRect = new Rect(); + public static final Rect tmpRect2 = new Rect(); + + /** Scales a rect about its centroid */ + public static void scaleRectAboutCenter(Rect r, float scale) { + if (scale != 1.0f) { + int cx = r.centerX(); + int cy = r.centerY(); + r.offset(-cx, -cy); + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + r.offset(cx, cy); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java new file mode 100644 index 0000000..5893abc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Context; + +import java.util.ArrayList; + + +/** + * The full recents space is partitioned using a BSP into various nodes that define where task + * stacks should be placed. + */ +public class SpaceNode { + Context mContext; + + SpaceNode mStartNode; + SpaceNode mEndNode; + + TaskStack mStack; + + public SpaceNode(Context context) { + mContext = context; + } + + /** Sets the current stack for this space node */ + public void setStack(TaskStack stack) { + mStack = stack; + } + + /** Returns the task stack (not null if this is a leaf) */ + TaskStack getStack() { + return mStack; + } + + /** Returns whether this is a leaf node */ + boolean isLeafNode() { + return (mStartNode == null) && (mEndNode == null); + } + + /** Returns all the descendent task stacks */ + private void getStacksRec(ArrayList<TaskStack> stacks) { + if (isLeafNode()) { + stacks.add(mStack); + } else { + mStartNode.getStacksRec(stacks); + mEndNode.getStacksRec(stacks); + } + } + public ArrayList<TaskStack> getStacks() { + ArrayList<TaskStack> stacks = new ArrayList<TaskStack>(); + getStacksRec(stacks); + return stacks; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java new file mode 100644 index 0000000..31b02e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.graphics.Rect; + + +/* BSP node callbacks */ +public interface SpaceNodeCallbacks { + /** Notifies when a node is added */ + public void onSpaceNodeAdded(SpaceNode node); + /** Notifies when a node is measured */ + public void onSpaceNodeMeasured(SpaceNode node, Rect rect); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java new file mode 100644 index 0000000..9b03c5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import com.android.systemui.recents.Constants; + + +/** + * A task represents the top most task in the system's task stack. + */ +public class Task { + public final int id; + public final Intent intent; + public String title; + public Drawable icon; + public Bitmap thumbnail; + + TaskCallbacks mCb; + + public Task(int id, Intent intent, String activityTitle, Drawable icon, Bitmap thumbnail) { + this.id = id; + this.intent = intent; + this.title = activityTitle; + this.icon = icon; + this.thumbnail = thumbnail; + } + + /** Set the callbacks */ + public void setCallbacks(TaskCallbacks cb) { + mCb = cb; + } + + /** Notifies the callback listeners that this task's data has changed */ + public void notifyTaskDataChanged() { + if (mCb != null) { + mCb.onTaskDataChanged(this); + } + } + + @Override + public boolean equals(Object o) { + // If we have multiple task entries for the same task, then we do the simple object + // equality check + if (Constants.Values.RecentsTaskLoader.TaskEntryMultiplier > 1) { + return super.equals(o); + } + + // Otherwise, check that the id and intent match (the other fields can be asynchronously + // loaded and is unsuitable to testing the identity of this Task) + Task t = (Task) o; + return (id == t.id) && + (intent.equals(t.intent)); + } + + @Override + public String toString() { + return "Task: " + intent.getComponent().getPackageName() + " [" + super.toString() + "]"; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java new file mode 100644 index 0000000..169f56c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +/* Task callbacks */ +public interface TaskCallbacks { + /* Notifies when a task's data has been updated */ + public void onTaskDataChanged(Task task); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java new file mode 100644 index 0000000..a5aa387 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.List; + + +/** + * An interface for a task filter to query whether a particular task should show in a stack. + */ +interface TaskFilter { + /** Returns whether the filter accepts the specified task */ + public boolean acceptTask(Task t, int index); +} + +/** + * A list of filtered tasks. + */ +class FilteredTaskList { + ArrayList<Task> mTasks = new ArrayList<Task>(); + ArrayList<Task> mFilteredTasks = new ArrayList<Task>(); + TaskFilter mFilter; + + /** Sets the task filter, saving the current touch state */ + void setFilter(TaskFilter filter) { + mFilter = filter; + updateFilteredTasks(); + } + + /** Removes the task filter and returns the previous touch state */ + void removeFilter() { + mFilter = null; + updateFilteredTasks(); + } + + /** Adds a new task to the task list */ + void add(Task t) { + mTasks.add(t); + updateFilteredTasks(); + } + + /** Sets the list of tasks */ + void set(List<Task> tasks) { + mTasks.clear(); + mTasks.addAll(tasks); + updateFilteredTasks(); + } + + /** Removes a task from the base list only if it is in the filtered list */ + boolean remove(Task t) { + if (mFilteredTasks.contains(t)) { + boolean removed = mTasks.remove(t); + updateFilteredTasks(); + return removed; + } + return false; + } + + /** Returns the index of this task in the list of filtered tasks */ + int indexOf(Task t) { + return mFilteredTasks.indexOf(t); + } + + /** Returns the size of the list of filtered tasks */ + int size() { + return mFilteredTasks.size(); + } + + /** Returns whether the filtered list contains this task */ + boolean contains(Task t) { + return mFilteredTasks.contains(t); + } + + /** Updates the list of filtered tasks whenever the base task list changes */ + private void updateFilteredTasks() { + mFilteredTasks.clear(); + if (mFilter != null) { + int taskCount = mTasks.size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTasks.get(i); + if (mFilter.acceptTask(t, i)) { + mFilteredTasks.add(t); + } + } + } else { + mFilteredTasks.addAll(mTasks); + } + } + + /** Returns whether this task list is filtered */ + boolean hasFilter() { + return (mFilter != null); + } + + /** Returns the list of filtered tasks */ + ArrayList<Task> getTasks() { + return mFilteredTasks; + } +} + +/** + * The task stack contains a list of multiple tasks. + */ +public class TaskStack { + Context mContext; + + FilteredTaskList mTaskList = new FilteredTaskList(); + TaskStackCallbacks mCb; + + public TaskStack(Context context) { + mContext = context; + } + + /** Sets the callbacks for this task stack */ + public void setCallbacks(TaskStackCallbacks cb) { + mCb = cb; + } + + /** Adds a new task */ + public void addTask(Task t) { + mTaskList.add(t); + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + + /** Removes a task */ + public void removeTask(Task t) { + if (mTaskList.contains(t)) { + mTaskList.remove(t); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + } + + /** Sets a few tasks in one go */ + public void setTasks(List<Task> tasks) { + int taskCount = mTaskList.getTasks().size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTaskList.getTasks().get(i); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + mTaskList.set(tasks); + for (Task t : tasks) { + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + } + + /** Gets the tasks */ + public ArrayList<Task> getTasks() { + return mTaskList.getTasks(); + } + + /** Gets the number of tasks */ + public int getTaskCount() { + return mTaskList.size(); + } + + /** Returns the index of this task in this current task stack */ + public int indexOfTask(Task t) { + return mTaskList.indexOf(t); + } + + /** Tests whether a task is in this current task stack */ + public boolean containsTask(Task t) { + return mTaskList.contains(t); + } + + /** Filters the stack into tasks similar to the one specified */ + public void filterTasks(Task t) { + // Set the task list filter + // XXX: This is a dummy filter that currently just accepts every other task. + mTaskList.setFilter(new TaskFilter() { + @Override + public boolean acceptTask(Task t, int i) { + if (i % 2 == 0) { + return true; + } + return false; + } + }); + if (mCb != null) { + mCb.onStackFiltered(this); + } + } + + /** Unfilters the current stack */ + public void unfilterTasks() { + // Unset the filter, then update the virtual scroll + mTaskList.removeFilter(); + if (mCb != null) { + mCb.onStackUnfiltered(this); + } + } + + /** Returns whether tasks are currently filtered */ + public boolean hasFilteredTasks() { + return mTaskList.hasFilter(); + } + + @Override + public String toString() { + String str = "Tasks:\n"; + for (Task t : mTaskList.getTasks()) { + str += " " + t.toString() + "\n"; + } + return str; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java new file mode 100644 index 0000000..4bec655 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.model; + +/* Task stack callbacks */ +public interface TaskStackCallbacks { + /* Notifies when a task has been added to the stack */ + public void onStackTaskAdded(TaskStack stack, Task t); + /* Notifies when a task has been removed from the stack */ + public void onStackTaskRemoved(TaskStack stack, Task t); + /** Notifies when the stack was filtered */ + public void onStackFiltered(TaskStack stack); + /** Notifies when the stack was un-filtered */ + public void onStackUnfiltered(TaskStack stack); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java new file mode 100644 index 0000000..c92041c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.UserHandle; +import android.view.View; +import android.widget.FrameLayout; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.ArrayList; + + +/** + * This view is the the top level layout that contains TaskStacks (which are laid out according + * to their SpaceNode bounds. + */ +public class RecentsView extends FrameLayout implements TaskStackViewCallbacks { + // The space partitioning root of this container + SpaceNode mBSP; + + public RecentsView(Context context) { + super(context); + setWillNotDraw(false); + } + + /** Set/get the bsp root node */ + public void setBSP(SpaceNode n) { + mBSP = n; + + // XXX: We shouldn't be recereating new stacks every time, but for now, that is OK + // Add all the stacks for this partition + removeAllViews(); + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + TaskStackView stackView = new TaskStackView(getContext(), stack); + stackView.setCallbacks(this); + addView(stackView); + } + } + + /** Launches the first task from the first stack if possible */ + public boolean launchFirstTask() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskStackView stackView = (TaskStackView) getChildAt(i); + TaskStack stack = stackView.mStack; + ArrayList<Task> tasks = stack.getTasks(); + if (!tasks.isEmpty()) { + Task task = tasks.get(tasks.size() - 1); + TaskView tv = null; + if (stackView.getChildCount() > 0) { + TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1); + if (stv.getTask() == task) { + tv = stv; + } + } + onTaskLaunched(stackView, tv, stack, task); + return true; + } + } + return false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|measure]", "width: " + width + " height: " + height, Console.AnsiGreen); + + // We measure our stack views sans the status bar. It will handle the nav bar itself. + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int childHeight = height - config.systemInsets.top; + + // Measure each child + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.measure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(childHeight, heightMode)); + } + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|layout]", new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen); + // We offset our stack views by the status bar height. It will handle the nav bar itself. + RecentsConfiguration config = RecentsConfiguration.getInstance(); + top += config.systemInsets.top; + + // Layout each child + // XXX: Based on the space node for that task view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + child.layout(left, top, left + width, top + height); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[RecentsView|dispatchDraw]", "", Console.AnsiPurple); + super.dispatchDraw(canvas); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen); + + // Update the configuration with the latest system insets and trigger a relayout + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.updateSystemInsets(insets); + requestLayout(); + + return true; + } + + /** Unfilters any filtered stacks */ + public boolean unfilterFilteredStacks() { + if (mBSP != null) { + // Check if there are any filtered stacks and unfilter them before we back out of Recents + boolean stacksUnfiltered = false; + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + if (stack.hasFilteredTasks()) { + stack.unfilterTasks(); + stacksUnfiltered = true; + } + } + return stacksUnfiltered; + } + return false; + } + + /**** View.OnClickListener Implementation ****/ + + @Override + public void onTaskLaunched(final TaskStackView stackView, final TaskView tv, + final TaskStack stack, final Task task) { + final Runnable launchRunnable = new Runnable() { + @Override + public void run() { + TaskViewTransform transform; + View sourceView = tv; + int offsetX = 0; + int offsetY = 0; + if (tv == null) { + // Launch the activity + sourceView = stackView; + transform = stackView.getStackTransform(stack.indexOfTask(task)); + offsetX = transform.rect.left; + offsetY = transform.rect.top; + } else { + transform = stackView.getStackTransform(stack.indexOfTask(task)); + } + + // Compute the thumbnail to scale up from + ActivityOptions opts = null; + int thumbnailWidth = transform.rect.width(); + int thumbnailHeight = transform.rect.height(); + if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 && + task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { + // Resize the thumbnail to the size of the view that we are animating from + Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + c.drawBitmap(task.thumbnail, + new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()), + new Rect(0, 0, thumbnailWidth, thumbnailHeight), null); + c.setBitmap(null); + opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView, + b, offsetX, offsetY); + } + + // Launch the activity with the desired animation + Intent i = new Intent(task.intent); + i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY + | Intent.FLAG_ACTIVITY_TASK_ON_HOME + | Intent.FLAG_ACTIVITY_NEW_TASK); + if (opts != null) { + getContext().startActivityAsUser(i, opts.toBundle(), UserHandle.CURRENT); + } else { + getContext().startActivityAsUser(i, UserHandle.CURRENT); + } + } + }; + + // Launch the app right away if there is no task view, otherwise, animate the icon out first + if (tv == null || !Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingRecents) { + launchRunnable.run(); + } else { + tv.animateOnLeavingRecents(launchRunnable); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java new file mode 100644 index 0000000..fe661bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.LinearInterpolator; + +/** + * This class facilitates swipe to dismiss. It defines an interface to be implemented by the + * by the class hosting the views that need to swiped, and, using this interface, handles touch + * events and translates / fades / animates the view as it is dismissed. + */ +public class SwipeHelper { + static final String TAG = "SwipeHelper"; + private static final boolean SLOW_ANIMATIONS = false; // DEBUG; + private static final boolean CONSTRAIN_SWIPE = true; + private static final boolean FADE_OUT_DURING_SWIPE = true; + private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; + + public static final int X = 0; + public static final int Y = 1; + + private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); + + private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec + private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms + private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms + private int MAX_DISMISS_VELOCITY = 2000; // dp/sec + private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms + + public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width + // where fade starts + static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width + // beyond which alpha->0 + private float mMinAlpha = 0f; + + private float mPagingTouchSlop; + Callback mCallback; + private int mSwipeDirection; + private VelocityTracker mVelocityTracker; + + private float mInitialTouchPos; + private boolean mDragging; + + private View mCurrView; + private boolean mCanCurrViewBeDimissed; + private float mDensityScale; + + public boolean mAllowSwipeTowardsStart = true; + public boolean mAllowSwipeTowardsEnd = true; + private boolean mRtl; + + public SwipeHelper(int swipeDirection, Callback callback, float densityScale, + float pagingTouchSlop) { + mCallback = callback; + mSwipeDirection = swipeDirection; + mVelocityTracker = VelocityTracker.obtain(); + mDensityScale = densityScale; + mPagingTouchSlop = pagingTouchSlop; + } + + public void setDensityScale(float densityScale) { + mDensityScale = densityScale; + } + + public void setPagingTouchSlop(float pagingTouchSlop) { + mPagingTouchSlop = pagingTouchSlop; + } + + public void cancelOngoingDrag() { + if (mDragging) { + if (mCurrView != null) { + mCallback.onDragCancelled(mCurrView); + setTranslation(mCurrView, 0); + mCallback.onSnapBackCompleted(mCurrView); + mCurrView = null; + } + mDragging = false; + } + } + + public void resetTranslation(View v) { + setTranslation(v, 0); + } + + private float getPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getX() : ev.getY(); + } + + private float getTranslation(View v) { + return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); + } + + private float getVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getXVelocity() : + vt.getYVelocity(); + } + + private ObjectAnimator createTranslationAnimation(View v, float newPos) { + ObjectAnimator anim = ObjectAnimator.ofFloat(v, + mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); + return anim; + } + + private float getPerpendicularVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getYVelocity() : + vt.getXVelocity(); + } + + private void setTranslation(View v, float translate) { + if (mSwipeDirection == X) { + v.setTranslationX(translate); + } else { + v.setTranslationY(translate); + } + } + + private float getSize(View v) { + final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics(); + return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels; + } + + public void setMinAlpha(float minAlpha) { + mMinAlpha = minAlpha; + } + + float getAlphaForOffset(View view) { + float viewSize = getSize(view); + final float fadeSize = ALPHA_FADE_END * viewSize; + float result = 1.0f; + float pos = getTranslation(view); + if (pos >= viewSize * ALPHA_FADE_START) { + result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; + } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { + result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; + } + result = Math.min(result, 1.0f); + result = Math.max(result, 0f); + return Math.max(mMinAlpha, result); + } + + /** + * Determines whether the given view has RTL layout. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isLayoutRtl(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); + } else { + return false; + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mDragging = false; + mCurrView = mCallback.getChildAtPosition(ev); + mVelocityTracker.clear(); + if (mCurrView != null) { + mRtl = isLayoutRtl(mCurrView); + mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); + mVelocityTracker.addMovement(ev); + mInitialTouchPos = getPos(ev); + } else { + mCanCurrViewBeDimissed = false; + } + break; + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + mVelocityTracker.addMovement(ev); + float pos = getPos(ev); + float delta = pos - mInitialTouchPos; + if (Math.abs(delta) > mPagingTouchSlop) { + mCallback.onBeginDrag(mCurrView); + mDragging = true; + mInitialTouchPos = getPos(ev) - getTranslation(mCurrView); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDragging = false; + mCurrView = null; + break; + } + return mDragging; + } + + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + */ + private void dismissChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + float newPos; + if (velocity < 0 + || (velocity == 0 && getTranslation(view) < 0) + // if we use the Menu to dismiss an item in landscape, animate up + || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { + newPos = -getSize(view); + } else { + newPos = getSize(view); + } + int duration = MAX_ESCAPE_ANIMATION_DURATION; + if (velocity != 0) { + duration = Math.min(duration, + (int) (Math.abs(newPos - getTranslation(view)) * + 1000f / Math.abs(velocity))); + } else { + duration = DEFAULT_ESCAPE_ANIMATION_DURATION; + } + + ValueAnimator anim = createTranslationAnimation(view, newPos); + anim.setInterpolator(sLinearInterpolator); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.onChildDismissed(view); + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.f); + } + } + }); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.start(); + } + + private void snapChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + ValueAnimator anim = createTranslationAnimation(view, 0); + int duration = SNAP_ANIM_LEN; + anim.setDuration(duration); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.0f); + } + mCallback.onSnapBackCompleted(view); + } + }); + anim.start(); + } + + public boolean onTouchEvent(MotionEvent ev) { + if (!mDragging) { + if (!onInterceptTouchEvent(ev)) { + return mCanCurrViewBeDimissed; + } + } + + mVelocityTracker.addMovement(ev); + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + float delta = getPos(ev) - mInitialTouchPos; + setSwipeAmount(delta); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mCurrView != null) { + endSwipe(mVelocityTracker); + } + break; + } + return true; + } + + private void setSwipeAmount(float amount) { + // don't let items that can't be dismissed be dragged more than + // maxScrollDistance + if (CONSTRAIN_SWIPE + && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { + float size = getSize(mCurrView); + float maxScrollDistance = 0.15f * size; + if (Math.abs(amount) >= size) { + amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2)); + } + } + setTranslation(mCurrView, amount); + if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { + float alpha = getAlphaForOffset(mCurrView); + mCurrView.setAlpha(alpha); + } + } + + private boolean isValidSwipeDirection(float amount) { + if (mSwipeDirection == X) { + if (mRtl) { + return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; + } else { + return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; + } + } + + // Vertical swipes are always valid. + return true; + } + + private void endSwipe(VelocityTracker velocityTracker) { + float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; + velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); + float velocity = getVelocity(velocityTracker); + float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); + float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; + float translation = getTranslation(mCurrView); + // Decide whether to dismiss the current view + boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && + Math.abs(translation) > 0.6 * getSize(mCurrView); + boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && + (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && + (velocity > 0) == (translation > 0); + + boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) + && isValidSwipeDirection(translation) + && (childSwipedFastEnough || childSwipedFarEnough); + + if (dismissChild) { + // flingadingy + dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); + } else { + // snappity + mCallback.onDragCancelled(mCurrView); + snapChild(mCurrView, velocity); + } + } + + public interface Callback { + View getChildAtPosition(MotionEvent ev); + + boolean canChildBeDismissed(View v); + + void onBeginDrag(View v); + + void onChildDismissed(View v); + + void onSnapBackCompleted(View v); + + void onDragCancelled(View v); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java new file mode 100644 index 0000000..9dd6c0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -0,0 +1,1075 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import android.widget.Toast; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.RecentsTaskLoader; +import com.android.systemui.recents.Utilities; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.model.TaskStackCallbacks; + +import java.util.ArrayList; + +/** The TaskView callbacks */ +interface TaskStackViewCallbacks { + public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t); +} + +/* The visual representation of a task stack view */ +public class TaskStackView extends FrameLayout implements TaskStackCallbacks, TaskViewCallbacks, + ViewPoolConsumer<TaskView, Task>, View.OnClickListener { + TaskStack mStack; + TaskStackViewTouchHandler mTouchHandler; + TaskStackViewCallbacks mCb; + ViewPool<TaskView, Task> mViewPool; + + // The various rects that define the stack view + Rect mRect = new Rect(); + Rect mStackRect = new Rect(); + Rect mStackRectSansPeek = new Rect(); + Rect mTaskRect = new Rect(); + + // The virtual stack scroll that we use for the card layout + int mStackScroll; + int mMinScroll; + int mMaxScroll; + OverScroller mScroller; + ObjectAnimator mScrollAnimator; + + // Optimizations + int mHwLayersRefCount; + int mStackViewsAnimationDuration; + boolean mStackViewsDirty = true; + boolean mAwaitingFirstLayout = true; + + public TaskStackView(Context context, TaskStack stack) { + super(context); + mStack = stack; + mStack.setCallbacks(this); + mScroller = new OverScroller(context); + mTouchHandler = new TaskStackViewTouchHandler(context, this); + mViewPool = new ViewPool<TaskView, Task>(context, this); + } + + /** Sets the callbacks */ + void setCallbacks(TaskStackViewCallbacks cb) { + mCb = cb; + } + + /** Requests that the views be synchronized with the model */ + void requestSynchronizeStackViewsWithModel() { + requestSynchronizeStackViewsWithModel(0); + } + void requestSynchronizeStackViewsWithModel(int duration) { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|requestSynchronize]", "", Console.AnsiYellow); + if (!mStackViewsDirty) { + invalidate(); + } + if (mAwaitingFirstLayout) { + // Skip the animation if we are awaiting first layout + mStackViewsAnimationDuration = 0; + } else { + mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); + } + mStackViewsDirty = true; + } + + // XXX: Optimization: Use a mapping of Task -> View + private TaskView getChildViewForTask(Task t) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + return tv; + } + } + return null; + } + + /** Update/get the transform */ + public TaskViewTransform getStackTransform(int indexInStack) { + TaskViewTransform transform = new TaskViewTransform(); + + // Map the items to an continuous position relative to the current scroll + int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards; + float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); + float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + float t = ((indexInStack * overlapHeight) - getStackScroll()) / overlapHeight; + float boundedT = Math.max(t, -(numPeekCards + 1)); + + // Set the scale relative to its position + float minScale = Constants.Values.TaskStackView.StackPeekMinScale; + float scaleRange = 1f - minScale; + float scaleInc = scaleRange / numPeekCards; + float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc))); + float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2; + transform.scale = scale; + + // Set the translation + if (boundedT < 0f) { + transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) / + numPeekCards) * peekHeight - scaleYOffset); + } else { + transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset); + } + + // Update the rect and visibility + transform.rect.set(mTaskRect); + if (t < -(numPeekCards + 1)) { + transform.visible = false; + } else { + transform.rect.offset(0, transform.translationY); + Utilities.scaleRectAboutCenter(transform.rect, scale); + transform.visible = Rect.intersects(mRect, transform.rect); + } + transform.t = t; + return transform; + } + + /** Synchronizes the views with the model */ + void synchronizeStackViewsWithModel() { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|synchronizeViewsWithModel]", + "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow); + if (mStackViewsDirty) { + + // XXX: Optimization: Use binary search to find the visible range + // XXX: Optimize to not call getStackTransform() so many times + // XXX: Consider using TaskViewTransform pool to prevent allocations + // XXX: Iterate children views, update transforms and remove all that are not visible + // For all remaining tasks, update transforms and if visible add the view + + // Update the visible state of all the tasks + ArrayList<Task> tasks = mStack.getTasks(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + Task task = tasks.get(i); + TaskViewTransform transform = getStackTransform(i); + TaskView tv = getChildViewForTask(task); + + if (transform.visible) { + if (tv == null) { + tv = mViewPool.pickUpViewFromPool(task, task); + // When we are picking up a new view from the view pool, prepare it for any + // following animation by putting it in a reasonable place + if (mStackViewsAnimationDuration > 0 && i != 0) { + // XXX: We have to animate when filtering, etc. Maybe we should have a + // runnable that ensures that tasks are animated in a special way + // when they are entering the scene? + int fromIndex = (transform.t < 0) ? (i - 1) : (i + 1); + tv.updateViewPropertiesFromTask(null, getStackTransform(fromIndex), 0); + } + } + } else { + if (tv != null) { + mViewPool.returnViewToPool(tv); + } + } + } + + // Update all the current view children + // NOTE: We have to iterate in reverse where because we are removing views directly + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + Task task = tv.getTask(); + TaskViewTransform transform = getStackTransform(mStack.indexOfTask(task)); + if (!transform.visible) { + mViewPool.returnViewToPool(tv); + } else { + tv.updateViewPropertiesFromTask(null, transform, mStackViewsAnimationDuration); + } + } + + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + " [TaskStackView|viewChildren]", "" + getChildCount()); + + mStackViewsAnimationDuration = 0; + mStackViewsDirty = false; + } + } + + /** Sets the current stack scroll */ + public void setStackScroll(int value) { + mStackScroll = value; + requestSynchronizeStackViewsWithModel(); + } + + /** Gets the current stack scroll */ + public int getStackScroll() { + return mStackScroll; + } + + /** Animates the stack scroll into bounds */ + ObjectAnimator animateBoundScroll(int duration) { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + // Enable hw layers on the stack + addHwLayersRefCount(); + + // Abort any current animations + mScroller.abortAnimation(); + if (mScrollAnimator != null) { + mScrollAnimator.cancel(); + mScrollAnimator.removeAllListeners(); + } + + // Start a new scroll animation + mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll); + mScrollAnimator.setDuration(duration); + mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setStackScroll((Integer) animation.getAnimatedValue()); + } + }); + mScrollAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Disable hw layers on the stack + decHwLayersRefCount(); + } + }); + mScrollAnimator.start(); + } + return mScrollAnimator; + } + + /** Aborts any current stack scrolls */ + void abortBoundScrollAnimation() { + if (mScrollAnimator != null) { + mScrollAnimator.cancel(); + } + } + + /** Bounds the current scroll if necessary */ + public boolean boundScroll() { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + setStackScroll(newScroll); + return true; + } + return false; + } + + /** Returns whether the current scroll is out of bounds */ + boolean isScrollOutOfBounds() { + return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll); + } + + /** Updates the min and max virtual scroll bounds */ + void updateMinMaxScroll(boolean boundScrollToNewMinMax) { + // Compute the min and max scroll values + int numTasks = Math.max(1, mStack.getTaskCount()); + int taskHeight = mTaskRect.height(); + int stackHeight = mStackRectSansPeek.height(); + int maxScrollHeight = taskHeight + (int) ((numTasks - 1) * + Constants.Values.TaskStackView.StackOverlapPct * taskHeight); + mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight; + mMaxScroll = maxScrollHeight - stackHeight; + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|minScroll] " + mMinScroll); + Console.log(" [TaskStack|maxScroll] " + mMaxScroll); + } + + if (boundScrollToNewMinMax) { + boundScroll(); + } + } + + /** Enables the hw layers and increments the hw layer requirement ref count */ + void addHwLayersRefCount() { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|addHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount + 1)); + if (mHwLayersRefCount == 0) { + // Enable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.enableHwLayers(); + } + } + mHwLayersRefCount++; + } + + /** Decrements the hw layer requirement ref count and disables the hw layers when we don't + need them anymore. */ + void decHwLayersRefCount() { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|decHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount - 1)); + mHwLayersRefCount--; + if (mHwLayersRefCount == 0) { + // Disable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.disableHwLayers(); + } + } else if (mHwLayersRefCount < 0) { + throw new RuntimeException("Invalid hw layers ref count"); + } + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + setStackScroll(mScroller.getCurrY()); + invalidate(); + + // If we just finished scrolling, then disable the hw layers + if (mScroller.isFinished()) { + decHwLayersRefCount(); + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return mTouchHandler.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mTouchHandler.onTouchEvent(ev); + } + + @Override + public void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "", + Console.AnsiPurple); + synchronizeStackViewsWithModel(); + super.dispatchDraw(canvas); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (Constants.DebugFlags.App.EnableTaskStackClipping) { + TaskView tv = (TaskView) child; + TaskView nextTv = null; + int curIndex = indexOfChild(tv); + if (curIndex < (getChildCount() - 1)) { + // Clip against the next view (if we aren't animating its alpha) + nextTv = (TaskView) getChildAt(curIndex + 1); + if (nextTv.getAlpha() == 1f) { + Rect curRect = tv.getClippingRect(Utilities.tmpRect, false); + Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + // The hit rects are relative to the task view, which needs to be offset by the + // system bar height + curRect.offset(0, config.systemInsets.top); + nextRect.offset(0, config.systemInsets.top); + // Compute the clip region + Region clipRegion = new Region(); + clipRegion.op(curRect, Region.Op.UNION); + clipRegion.op(nextRect, Region.Op.DIFFERENCE); + // Clip the canvas + int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRegion(clipRegion); + boolean invalidate = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(saveCount); + return invalidate; + } + } + } + return super.drawChild(canvas, child, drawingTime); + } + + /** Computes the stack and task rects */ + public void computeRects(int width, int height) { + // Note: We let the stack view be the full height because we want the cards to go under the + // navigation bar if possible. However, the stack rects which we use to calculate + // max scroll, etc. need to take the nav bar into account + + // Compute the stack rects + RecentsConfiguration config = RecentsConfiguration.getInstance(); + mRect.set(0, 0, width, height); + mStackRect.set(mRect); + mStackRect.bottom -= config.systemInsets.bottom; + + int smallestDimension = Math.min(width, height); + int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f); + mStackRect.inset(padding, padding); + mStackRectSansPeek.set(mStackRect); + mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + + // Compute the task rect + if (RecentsConfiguration.getInstance().layoutVerticalStack) { + int minHeight = (int) (mStackRect.height() - + (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height())); + int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height())); + int centerX = mStackRect.centerX(); + mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top, + centerX + size / 2, mStackRectSansPeek.top + size); + } else { + int size = Math.min(mStackRect.width(), mStackRect.height()); + int centerY = mStackRect.centerY(); + mTaskRect.set(mStackRectSansPeek.top, centerY - size / 2, + mStackRectSansPeek.top + size, centerY + size / 2); + } + + // Update the scroll bounds + updateMinMaxScroll(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]", + "width: " + width + " height: " + height + + " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen); + + // Compute our stack/task rects + computeRects(width, height); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // If this is the first layout, then scroll to the front of the stack and synchronize the + // stack views immediately + if (mAwaitingFirstLayout) { + setStackScroll(mMaxScroll); + requestSynchronizeStackViewsWithModel(); + synchronizeStackViewsWithModel(); + + // Animate the icon of the first task view + if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) { + TaskView tv = (TaskView) getChildAt(getChildCount() - 1); + if (tv != null) { + tv.animateOnEnterRecents(); + } + } + } + + // Measure each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY)); + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]", + "" + new Rect(left, top, right, bottom), Console.AnsiGreen); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // Layout each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.layout(mTaskRect.left, mStackRectSansPeek.top, + mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height()); + } + + if (!mAwaitingFirstLayout) { + requestSynchronizeStackViewsWithModel(); + } else { + mAwaitingFirstLayout = false; + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + requestSynchronizeStackViewsWithModel(); + } + + public boolean isTransformedTouchPointInView(float x, float y, View child) { + return isTransformedTouchPointInView(x, y, child, null); + } + + /**** TaskStackCallbacks Implementation ****/ + + @Override + public void onStackTaskAdded(TaskStack stack, Task t) { + requestSynchronizeStackViewsWithModel(); + } + + @Override + public void onStackTaskRemoved(TaskStack stack, Task t) { + // Remove the view associated with this task, we can't rely on updateTransforms + // to work here because the task is no longer in the list + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + mViewPool.returnViewToPool(tv); + break; + } + } + + updateMinMaxScroll(true); + requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration); + } + + @Override + public void onStackFiltered(TaskStack stack) { + requestSynchronizeStackViewsWithModel(); + } + + @Override + public void onStackUnfiltered(TaskStack stack) { + requestSynchronizeStackViewsWithModel(); + } + + /**** ViewPoolConsumer Implementation ****/ + + @Override + public TaskView createView(Context context) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]"); + return new TaskView(context); + } + + @Override + public void prepareViewToEnterPool(TaskView tv) { + Task task = tv.getTask(); + tv.resetViewProperties(); + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]", + tv.getTask() + " tv: " + tv); + + // Report that this tasks's data is no longer being used + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.unloadTaskData(task); + tv.unbindFromTask(); + + // Detach the view from the hierarchy + detachViewFromParent(tv); + + // Disable hw layers on this view + tv.disableHwLayers(); + } + + @Override + public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]", + "isNewView: " + isNewView); + + // Setup and attach the view to the window + Task task = prepareData; + // We try and rebind the task (this MUST be done before the task filled) + tv.bindToTask(task, this); + // Request that this tasks's data be filled + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.loadTaskData(task); + tv.syncToTask(); + + // Find the index where this task should be placed in the children + int insertIndex = -1; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + Task tvTask = ((TaskView) getChildAt(i)).getTask(); + if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) { + insertIndex = i; + break; + } + } + + // Add/attach the view to the hierarchy + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]", + "" + insertIndex); + if (isNewView) { + addView(tv, insertIndex); + tv.setOnClickListener(this); + } else { + attachViewToParent(tv, insertIndex, tv.getLayoutParams()); + } + + // Enable hw layers on this view if hw layers are enabled on the stack + if (mHwLayersRefCount > 0) { + tv.enableHwLayers(); + } + } + + @Override + public boolean hasPreferredData(TaskView tv, Task preferredData) { + return (tv.getTask() == preferredData); + } + + /**** TaskViewCallbacks Implementation ****/ + + @Override + public void onTaskIconClicked(TaskView tv) { + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]", + tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(), + Console.AnsiCyan); + if (Constants.DebugFlags.App.EnableTaskFiltering) { + if (mStack.hasFilteredTasks()) { + mStack.unfilterTasks(); + } else { + mStack.filterTasks(tv.getTask()); + } + } else { + Toast.makeText(getContext(), "Task Filtering TBD", Toast.LENGTH_SHORT).show(); + } + } + + /**** View.OnClickListener Implementation ****/ + + @Override + public void onClick(View v) { + TaskView tv = (TaskView) v; + Task task = tv.getTask(); + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]", + task + " cb: " + mCb); + + if (mCb != null) { + mCb.onTaskLaunched(this, tv, mStack, task); + } + } +} + +/* Handles touch events */ +class TaskStackViewTouchHandler { + static int INACTIVE_POINTER_ID = -1; + + TaskStackView mSv; + VelocityTracker mVelocityTracker; + + boolean mIsScrolling; + boolean mIsSwiping; + + int mInitialMotionX, mInitialMotionY; + int mLastMotionX, mLastMotionY; + int mActivePointerId = INACTIVE_POINTER_ID; + TaskView mActiveTaskView = null; + + int mTotalScrollMotion; + int mMinimumVelocity; + int mMaximumVelocity; + // The scroll touch slop is used to calculate when we start scrolling + int mScrollTouchSlop; + // The swipe touch slop is used to calculate when we start swiping left/right, this takes + // precendence over the scroll touch slop in case the user makes a gesture that starts scrolling + // but is intended to be a swipe + int mSwipeTouchSlop; + // After a certain amount of scrolling, we should start ignoring checks for swiping + int mMaxScrollMotionToRejectSwipe; + + public TaskStackViewTouchHandler(Context context, TaskStackView sv) { + ViewConfiguration configuration = ViewConfiguration.get(context); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mScrollTouchSlop = configuration.getScaledTouchSlop(); + mSwipeTouchSlop = 2 * mScrollTouchSlop; + mMaxScrollMotionToRejectSwipe = 4 * mScrollTouchSlop; + mSv = sv; + } + + /** Velocity tracker helpers */ + void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** Returns the view at the specified coordinates */ + TaskView findViewAtPoint(int x, int y) { + int childCount = mSv.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) mSv.getChildAt(i); + if (tv.getVisibility() == View.VISIBLE) { + if (mSv.isTransformedTouchPointInView(x, y, tv)) { + return tv; + } + } + } + return null; + } + + /** Touch preprocessing for handling below */ + public boolean onInterceptTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|interceptTouchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + boolean wasScrolling = !mSv.mScroller.isFinished() || + (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning()); + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.mScroller.abortAnimation(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Check if the scroller is finished yet + mIsScrolling = !mSv.mScroller.isFinished(); + mIsSwiping = false; + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int y = (int) ev.getY(activePointerIndex); + int x = (int) ev.getX(activePointerIndex); + if (mActiveTaskView != null && + mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && + Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && + Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { + // Start swiping and stop scrolling + mIsScrolling = false; + mIsSwiping = true; + System.out.println("SWIPING: " + mActiveTaskView); + // Initialize the velocity tracker if necessary + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } else if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { + // Save the touch move info + mIsScrolling = true; + // Initialize the velocity tracker if necessary + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + + mLastMotionX = x; + mLastMotionY = y; + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + // Animate the scroll back if we've cancelled + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + // Reset the drag state and the velocity tracker + mIsScrolling = false; + mIsSwiping = false; + mActivePointerId = INACTIVE_POINTER_ID; + mActiveTaskView = null; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + break; + } + } + + return wasScrolling || mIsScrolling || mIsSwiping; + } + + /** Handles touch events once we have intercepted them */ + public boolean onTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackViewTouchHandler|touchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + // Short circuit if we have no children + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + // Update the velocity tracker + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.mScroller.abortAnimation(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // XXX: Set mIsScrolling or mIsSwiping? + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int x = (int) ev.getX(activePointerIndex); + int y = (int) ev.getY(activePointerIndex); + int deltaY = mLastMotionY - y; + int deltaX = x - mLastMotionX; + if (!mIsSwiping) { + if (mActiveTaskView != null && + mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && + Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && + Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { + mIsScrolling = false; + mIsSwiping = true; + System.out.println("SWIPING: " + mActiveTaskView); + // Initialize the velocity tracker if necessary + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } + if (!mIsSwiping && !mIsScrolling) { + if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { + mIsScrolling = true; + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } + if (mIsScrolling) { + mSv.setStackScroll(mSv.getStackScroll() + deltaY); + if (mSv.isScrollOutOfBounds()) { + mVelocityTracker.clear(); + } + } else if (mIsSwiping) { + mActiveTaskView.setTranslationX(mActiveTaskView.getTranslationX() + deltaX); + } + mLastMotionX = x; + mLastMotionY = y; + mTotalScrollMotion += Math.abs(deltaY); + break; + } + case MotionEvent.ACTION_UP: { + if (mIsScrolling || mIsSwiping) { + final TaskView activeTv = mActiveTaskView; + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + + if (mIsSwiping) { + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + // Fling to dismiss + int newScrollX = (int) (Math.signum(initialVelocity) * + activeTv.getMeasuredWidth()); + int duration = Math.min(Constants.Values.TaskStackView.Animation.SwipeDismissDuration, + (int) (Math.abs(newScrollX - activeTv.getScrollX()) * + 1000f / Math.abs(initialVelocity))); + activeTv.animate() + .translationX(newScrollX) + .alpha(0f) + .setDuration(duration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + Task task = activeTv.getTask(); + Activity activity = (Activity) mSv.getContext(); + + // We have to disable the listener to ensure that we + // don't hit this again + activeTv.animate().setListener(null); + + // Remove the task from the view + mSv.mStack.removeTask(task); + + // Remove any stored data from the loader + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.deleteTaskData(task); + + // Remove the task from activity manager + final ActivityManager am = (ActivityManager) + activity.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + am.removeTask(activeTv.getTask().id, + ActivityManager.REMOVE_TASK_KILL_PROCESS); + } + + // If there are no remaining tasks, then just close the activity + if (mSv.mStack.getTaskCount() == 0) { + activity.finish(); + } + + // Disable HW layers + mSv.decHwLayersRefCount(); + } + }) + .start(); + // Enable HW layers + mSv.addHwLayersRefCount(); + } else { + // Animate it back into place + // XXX: Make this animation a function of the velocity OR distance + int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; + activeTv.animate() + .translationX(0) + .setDuration(duration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Disable HW layers + mSv.decHwLayersRefCount(); + } + }) + .start(); + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } else { + int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); + if ((Math.abs(velocity) > mMinimumVelocity)) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|fling]", + "scroll: " + mSv.getStackScroll() + " velocity: " + velocity, + Console.AnsiGreen); + // Enable HW layers on the stack + mSv.addHwLayersRefCount(); + // Fling scroll + mSv.mScroller.fling(0, mSv.getStackScroll(), + 0, -velocity, + 0, 0, + mSv.mMinScroll, mSv.mMaxScroll, + 0, 0); + // Invalidate to kick off computeScroll + mSv.invalidate(); + } else if (mSv.isScrollOutOfBounds()) { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + } + } + } + + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mIsSwiping = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + // Disable HW layers + mSv.decHwLayersRefCount(); + break; + } + case MotionEvent.ACTION_CANCEL: { + if (mIsScrolling || mIsSwiping) { + if (mIsSwiping) { + // Animate it back into place + // XXX: Make this animation a function of the velocity OR distance + int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; + mActiveTaskView.animate() + .translationX(0) + .setDuration(duration) + .start(); + } else { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + } + } + + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mIsSwiping = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + // Disable HW layers + mSv.decHwLayersRefCount(); + break; + } + } + return true; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java new file mode 100644 index 0000000..b1d0d13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.view.Gravity; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskCallbacks; + +/** The TaskView callbacks */ +interface TaskViewCallbacks { + public void onTaskIconClicked(TaskView tv); + // public void onTaskViewReboundToTask(TaskView tv, Task t); +} + +/** The task thumbnail view */ +class TaskThumbnailView extends ImageView { + Task mTask; + int mBarColor; + + Path mRoundedRectClipPath = new Path(); + + public TaskThumbnailView(Context context) { + super(context); + setScaleType(ScaleType.FIT_XY); + } + + /** Binds the thumbnail view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + if (t.thumbnail != null) { + // Update the bar color + if (Constants.Values.TaskView.DrawColoredTaskBars) { + int[] colors = {0xFFCC0C39, 0xFFE6781E, 0xFFC8CF02, 0xFF1693A7}; + mBarColor = colors[mTask.intent.getComponent().getPackageName().length() % colors.length]; + } + + setImageBitmap(t.thumbnail); + if (animate) { + setAlpha(0f); + animate().alpha(1f) + .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration) + .start(); + } + } + } + + /** Unbinds the thumbnail view from the task */ + void unbindFromTask() { + mTask = null; + setImageDrawable(null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Update the rounded rect clip path + RecentsConfiguration config = RecentsConfiguration.getInstance(); + float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps); + mRoundedRectClipPath.reset(); + mRoundedRectClipPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), + radius, radius, Path.Direction.CW); + } + + @Override + protected void onDraw(Canvas canvas) { + if (Constants.Values.TaskView.UseRoundedCorners) { + canvas.clipPath(mRoundedRectClipPath); + } + + super.onDraw(canvas); + + if (Constants.Values.TaskView.DrawColoredTaskBars) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int taskBarHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps); + // XXX: If we actually use this, this should be pulled out into a TextView that we + // inflate + + // Draw the task bar + Rect r = new Rect(); + Paint p = new Paint(); + p.setAntiAlias(true); + p.setSubpixelText(true); + p.setColor(mBarColor); + p.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + canvas.drawRect(0, 0, getMeasuredWidth(), taskBarHeight, p); + p.setColor(0xFFffffff); + p.setTextSize(68); + p.getTextBounds("X", 0, 1, r); + int offset = (int) (taskBarHeight - r.height()) / 2; + canvas.drawText(mTask.title, offset, offset + r.height(), p); + } + } +} + +/* The task icon view */ +class TaskIconView extends ImageView { + Task mTask; + + Path mClipPath = new Path(); + float mClipRadius; + Point mClipOrigin = new Point(); + ObjectAnimator mCircularClipAnimator; + + public TaskIconView(Context context) { + super(context); + mClipPath = new Path(); + mClipRadius = 1f; + } + + /** Binds the icon view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + if (t.icon != null) { + setImageDrawable(t.icon); + if (animate) { + setAlpha(0f); + animate().alpha(1f) + .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration) + .start(); + } + } + } + + /** Unbinds the icon view from the task */ + void unbindFromTask() { + mTask = null; + setImageDrawable(null); + } + + /** Sets the circular clip radius on the icon */ + public void setCircularClipRadius(float r) { + Console.log(Constants.DebugFlags.UI.Clipping, "[TaskView|setCircularClip]", "" + r); + mClipRadius = r; + invalidate(); + } + + /** Gets the circular clip radius on the icon */ + public float getCircularClipRadius() { + return mClipRadius; + } + + /** Animates the circular clip radius on the icon */ + void animateCircularClip(boolean brNotTl, float newRadius, int duration, int startDelay, + TimeInterpolator interpolator, + AnimatorListenerAdapter listener) { + if (mCircularClipAnimator != null) { + mCircularClipAnimator.cancel(); + mCircularClipAnimator.removeAllListeners(); + } + if (brNotTl) { + mClipOrigin.set(0, 0); + } else { + mClipOrigin.set(getMeasuredWidth(), getMeasuredHeight()); + } + mCircularClipAnimator = ObjectAnimator.ofFloat(this, "circularClipRadius", newRadius); + mCircularClipAnimator.setStartDelay(startDelay); + mCircularClipAnimator.setDuration(duration); + mCircularClipAnimator.setInterpolator(interpolator); + if (listener != null) { + mCircularClipAnimator.addListener(listener); + } + mCircularClipAnimator.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + int maxSize = (int) Math.ceil(Math.sqrt(width * width + height * height)); + mClipPath.reset(); + mClipPath.addCircle(mClipOrigin.x, mClipOrigin.y, mClipRadius * maxSize, Path.Direction.CW); + canvas.clipPath(mClipPath); + super.onDraw(canvas); + canvas.restoreToCount(saveCount); + } +} + +/* A task view */ +public class TaskView extends FrameLayout implements View.OnClickListener, TaskCallbacks { + Task mTask; + TaskThumbnailView mThumbnailView; + TaskIconView mIconView; + TaskViewCallbacks mCb; + + public TaskView(Context context) { + super(context); + mThumbnailView = new TaskThumbnailView(context); + mIconView = new TaskIconView(context); + mIconView.setOnClickListener(this); + addView(mThumbnailView); + addView(mIconView); + + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int barHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps); + int iconSize = config.pxFromDp(Constants.Values.TaskView.TaskIconSizeDps); + int offset = barHeight - (iconSize / 2); + + // XXX: Lets keep the icon in the corner for the time being + offset = iconSize / 4; + + /* + ((LayoutParams) mThumbnailView.getLayoutParams()).leftMargin = barHeight / 2; + ((LayoutParams) mThumbnailView.getLayoutParams()).rightMargin = barHeight / 2; + ((LayoutParams) mThumbnailView.getLayoutParams()).bottomMargin = barHeight; + */ + ((LayoutParams) mIconView.getLayoutParams()).gravity = Gravity.END; + ((LayoutParams) mIconView.getLayoutParams()).width = iconSize; + ((LayoutParams) mIconView.getLayoutParams()).height = iconSize; + ((LayoutParams) mIconView.getLayoutParams()).topMargin = offset; + ((LayoutParams) mIconView.getLayoutParams()).rightMargin = offset; + } + + /** Set the task and callback */ + void bindToTask(Task t, TaskViewCallbacks cb) { + mTask = t; + mTask.setCallbacks(this); + mCb = cb; + } + + /** Actually synchronizes the model data into the views */ + void syncToTask() { + mThumbnailView.rebindToTask(mTask, false); + mIconView.rebindToTask(mTask, false); + } + + /** Unset the task and callback */ + void unbindFromTask() { + mTask.setCallbacks(null); + mThumbnailView.unbindFromTask(); + mIconView.unbindFromTask(); + } + + /** Gets the task */ + Task getTask() { + return mTask; + } + + /** Synchronizes this view's properties with the task's transform */ + void updateViewPropertiesFromTask(TaskViewTransform animateFromTransform, + TaskViewTransform transform, int duration) { + if (duration > 0) { + if (animateFromTransform != null) { + setTranslationY(animateFromTransform.translationY); + setScaleX(animateFromTransform.scale); + setScaleY(animateFromTransform.scale); + } + animate().translationY(transform.translationY) + .scaleX(transform.scale) + .scaleY(transform.scale) + .setDuration(duration) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } else { + setTranslationY(transform.translationY); + setScaleX(transform.scale); + setScaleY(transform.scale); + } + } + + /** Resets this view's properties */ + void resetViewProperties() { + setTranslationX(0f); + setTranslationY(0f); + setScaleX(1f); + setScaleY(1f); + setAlpha(1f); + } + + /** Animates this task view as it enters recents */ + public void animateOnEnterRecents() { + mIconView.setCircularClipRadius(0f); + mIconView.animateCircularClip(true, 1f, + Constants.Values.TaskView.Animation.TaskIconCircularClipInDuration, + 300, new AccelerateInterpolator(), null); + } + + /** Animates this task view as it exits recents */ + public void animateOnLeavingRecents(final Runnable r) { + if (Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingUseClip) { + mIconView.animateCircularClip(false, 0f, + Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration, 0, + new DecelerateInterpolator(), + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + r.run(); + } + }); + } else { + mIconView.animate() + .alpha(0f) + .setDuration(Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration) + .setInterpolator(new DecelerateInterpolator()) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + r.run(); + } + }) + .start(); + } + } + + /** Returns the rect we want to clip (it may not be the full rect) */ + Rect getClippingRect(Rect outRect, boolean accountForRoundedRects) { + getHitRect(outRect); + // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster + outRect.right = outRect.left + mThumbnailView.getRight(); + outRect.bottom = outRect.top + mThumbnailView.getBottom(); + // We need to shrink the next rect by the rounded corners since those are draw on + // top of the current view + if (accountForRoundedRects) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps); + outRect.inset((int) radius, (int) radius); + } + return outRect; + } + + /** Enable the hw layers on this task view */ + void enableHwLayers() { + Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|enableHwLayers]"); + mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + /** Disable the hw layers on this task view */ + void disableHwLayers() { + Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|disableHwLayers]"); + mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null); + } + + @Override + public void onTaskDataChanged(Task task) { + Console.log(Constants.DebugFlags.App.EnableBackgroundTaskLoading, + "[TaskView|onTaskDataChanged]", task); + + // Only update this task view if the changed task is the same as the task for this view + if (mTask == task) { + mThumbnailView.rebindToTask(mTask, true); + mIconView.rebindToTask(mTask, true); + } + } + + @Override + public void onClick(View v) { + mCb.onTaskIconClicked(this); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java new file mode 100644 index 0000000..66c52a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.graphics.Rect; + + +/* The transform state for a task view */ +public class TaskViewTransform { + public int translationY = 0; + public float scale = 1f; + public boolean visible = true; + public Rect rect = new Rect(); + float t; + + @Override + public String toString() { + return "TaskViewTransform y: " + translationY + " scale: " + scale + + " visible: " + visible + " rect: " + rect; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java new file mode 100644 index 0000000..f7d7095 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.content.Context; + +import java.util.Iterator; +import java.util.LinkedList; + + +/* A view pool to manage more views than we can visibly handle */ +public class ViewPool<V, T> { + Context mContext; + ViewPoolConsumer<V, T> mViewCreator; + LinkedList<V> mPool = new LinkedList<V>(); + + /** Initializes the pool with a fixed predetermined pool size */ + public ViewPool(Context context, ViewPoolConsumer<V, T> viewCreator) { + mContext = context; + mViewCreator = viewCreator; + } + + /** Returns a view into the pool */ + void returnViewToPool(V v) { + mViewCreator.prepareViewToEnterPool(v); + mPool.push(v); + } + + /** Gets a view from the pool and prepares it */ + V pickUpViewFromPool(T preferredData, T prepareData) { + V v = null; + boolean isNewView = false; + if (mPool.isEmpty()) { + v = mViewCreator.createView(mContext); + isNewView = true; + } else { + // Try and find a preferred view + Iterator<V> iter = mPool.iterator(); + while (iter.hasNext()) { + V vpv = iter.next(); + if (mViewCreator.hasPreferredData(vpv, preferredData)) { + v = vpv; + iter.remove(); + break; + } + } + // Otherwise, just grab the first view + if (v == null) { + v = mPool.pop(); + } + } + mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView); + return v; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java new file mode 100644 index 0000000..50f45bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recents.views; + +import android.content.Context; + + +/* An interface to the consumer of a view pool */ +public interface ViewPoolConsumer<V, T> { + public V createView(Context context); + public void prepareViewToEnterPool(V v); + public void prepareViewToLeavePool(V v, T prepareData, boolean isNewView); + public boolean hasPreferredData(V v, T preferredData); +} |