Initial changes for recents.
Change-Id: Ide2c202b4a5b25410f0f32bd0a81ccf817ede38f
diff --git a/api/current.txt b/api/current.txt
index 1e7d548..0a0f148 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1295,6 +1295,8 @@
field public static final int dialog_min_width_minor = 17104900; // 0x1050004
field public static final int notification_large_icon_height = 17104902; // 0x1050006
field public static final int notification_large_icon_width = 17104901; // 0x1050005
+ field public static final int recents_thumbnail_height = 17104903; // 0x1050007
+ field public static final int recents_thumbnail_width = 17104904; // 0x1050008
field public static final int thumbnail_height = 17104897; // 0x1050001
field public static final int thumbnail_width = 17104898; // 0x1050002
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index c877cd3..7f7616f 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -933,6 +933,16 @@
}
}
+ /** @hide */
+ public boolean isInHomeStack(int taskId) {
+ try {
+ return ActivityManagerNative.getDefault().isInHomeStack(taskId);
+ } catch (RemoteException e) {
+ // System dead, we will be dead too soon!
+ return false;
+ }
+ }
+
/**
* Flag for {@link #moveTaskToFront(int, int)}: also move the "home"
* activity along with the task, so it is positioned immediately behind
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index f4358e9..c7c81dd 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -654,6 +654,15 @@
return true;
}
+ case IS_IN_HOME_STACK_TRANSACTION: {
+ data.enforceInterface(IActivityManager.descriptor);
+ int taskId = data.readInt();
+ boolean isInHomeStack = isInHomeStack(taskId);
+ reply.writeNoException();
+ reply.writeInt(isInHomeStack ? 1 : 0);
+ return true;
+ }
+
case SET_FOCUSED_STACK_TRANSACTION: {
data.enforceInterface(IActivityManager.descriptor);
int stackId = data.readInt();
@@ -2833,6 +2842,19 @@
return info;
}
@Override
+ public boolean isInHomeStack(int taskId) throws RemoteException {
+ Parcel data = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ data.writeInterfaceToken(IActivityManager.descriptor);
+ data.writeInt(taskId);
+ mRemote.transact(IS_IN_HOME_STACK_TRANSACTION, data, reply, 0);
+ reply.readException();
+ boolean isInHomeStack = reply.readInt() > 0;
+ data.recycle();
+ reply.recycle();
+ return isInHomeStack;
+ }
+ @Override
public void setFocusedStack(int stackId) throws RemoteException
{
Parcel data = Parcel.obtain();
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 138eea2..9cfd85a 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3017,11 +3017,17 @@
int h;
if (w < 0) {
Resources res = r.activity.getResources();
- mThumbnailHeight = h =
- res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
-
- mThumbnailWidth = w =
- res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+ if (SystemProperties.getBoolean("persist.recents.use_alternate", false)) {
+ int wId = com.android.internal.R.dimen.recents_thumbnail_width;
+ int hId = com.android.internal.R.dimen.recents_thumbnail_height;
+ mThumbnailWidth = w = res.getDimensionPixelSize(wId);
+ mThumbnailHeight = h = res.getDimensionPixelSize(hId);
+ } else {
+ mThumbnailHeight = h =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+ mThumbnailWidth = w =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+ }
} else {
h = mThumbnailHeight;
}
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 1943bba..f2cabf4 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -122,6 +122,7 @@
public void resizeStack(int stackId, Rect bounds) throws RemoteException;
public List<StackInfo> getAllStackInfos() throws RemoteException;
public StackInfo getStackInfo(int stackId) throws RemoteException;
+ public boolean isInHomeStack(int taskId) throws RemoteException;
public void setFocusedStack(int stackId) throws RemoteException;
public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException;
/* oneway */
@@ -717,4 +718,5 @@
// Start of L transactions
int GET_TAG_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+210;
int START_USER_IN_BACKGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+211;
+ int IS_IN_HOME_STACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+212;
}
diff --git a/core/res/res/values-sw600dp/dimens.xml b/core/res/res/values-sw600dp/dimens.xml
index d21f9b7..8f83ab2 100644
--- a/core/res/res/values-sw600dp/dimens.xml
+++ b/core/res/res/values-sw600dp/dimens.xml
@@ -22,6 +22,10 @@
<dimen name="thumbnail_width">200dp</dimen>
<!-- The height that is used when creating thumbnails of applications. -->
<dimen name="thumbnail_height">177dp</dimen>
+ <!-- The width that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_width">512dp</dimen>
+ <!-- The height that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_height">512dp</dimen>
<!-- The maximum number of action buttons that should be permitted within
an action bar/action mode. This will be used to determine how many
showAsAction="ifRoom" items can fit. "always" items can override this. -->
diff --git a/core/res/res/values-sw720dp/dimens.xml b/core/res/res/values-sw720dp/dimens.xml
index ccdb4be..040bb5b 100644
--- a/core/res/res/values-sw720dp/dimens.xml
+++ b/core/res/res/values-sw720dp/dimens.xml
@@ -38,6 +38,10 @@
<dimen name="thumbnail_width">230dp</dimen>
<!-- The height that is used when creating thumbnails of applications. -->
<dimen name="thumbnail_height">135dp</dimen>
+ <!-- The width that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_width">512dp</dimen>
+ <!-- The height that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_height">512dp</dimen>
<!-- Preference activity, vertical padding for the header list -->
<dimen name="preference_screen_header_vertical_padding">32dp</dimen>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 8c8c322..8ec2e6f 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -22,6 +22,10 @@
<dimen name="thumbnail_width">164dp</dimen>
<!-- The height that is used when creating thumbnails of applications. -->
<dimen name="thumbnail_height">145dp</dimen>
+ <!-- The width that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_width">256dp</dimen>
+ <!-- The height that is used when creating thumbnails of applications. -->
+ <dimen name="recents_thumbnail_height">256dp</dimen>
<!-- The standard size (both width and height) of an application icon that
will be displayed in the app launcher and elsewhere. -->
<dimen name="app_icon_size">48dip</dimen>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index c9d203f..d58e8ad 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2108,6 +2108,9 @@
<public type="attr" name="requiredForProfile"/>
<public type="attr" name="pinned" />
+ <public type="dimen" name="recents_thumbnail_height" />
+ <public type="dimen" name="recents_thumbnail_width" />
+
<public type="id" name="shared_element_name" />
<public type="style" name="Widget.Holo.FragmentBreadCrumbs" />
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 @@
}
}
+ /** 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);
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 864211d..128f636 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7245,6 +7245,24 @@
}
@Override
+ public boolean isInHomeStack(int taskId) {
+ enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS,
+ "getStackInfo()");
+ long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (this) {
+ TaskRecord tr = recentTaskForIdLocked(taskId);
+ if (tr != null) {
+ return tr.stack.isHomeStack();
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return false;
+ }
+
+ @Override
public int getTaskForActivity(IBinder token, boolean onlyRoot) {
synchronized(this) {
return ActivityRecord.getTaskForActivityLocked(token, onlyRoot);
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index f3ccdd6..087ad83c 100755
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -71,6 +71,7 @@
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.util.EventLog;
@@ -715,10 +716,17 @@
int w = mThumbnailWidth;
int h = mThumbnailHeight;
if (w < 0) {
- mThumbnailWidth = w =
- res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
- mThumbnailHeight = h =
- res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+ if (SystemProperties.getBoolean("persist.recents.use_alternate", false)) {
+ mThumbnailWidth = w =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.recents_thumbnail_width);
+ mThumbnailHeight = h =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.recents_thumbnail_height);
+ } else {
+ mThumbnailWidth = w =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_width);
+ mThumbnailHeight = h =
+ res.getDimensionPixelSize(com.android.internal.R.dimen.thumbnail_height);
+ }
}
if (w > 0) {