Merge "Fixing memory leaks related to Tasks holding onto their callbacks."

This commit is contained in:
Winson Chung
2014-03-14 20:34:27 +00:00
committed by Android (Google) Code Review
11 changed files with 392 additions and 240 deletions

View File

@@ -17,6 +17,7 @@
package com.android.systemui.recents;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
@@ -36,20 +37,20 @@ public class Console {
/** Logs a key */
public static void log(String key) {
Console.log(true, key, "", AnsiReset);
log(true, key, "", AnsiReset);
}
/** Logs a conditioned key */
public static void log(boolean condition, String key) {
if (condition) {
Console.log(condition, key, "", AnsiReset);
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);
log(condition, key, data, AnsiReset);
}
}
@@ -74,6 +75,50 @@ public class Console {
}
}
/** Logs a stack trace */
public static void logStackTrace() {
logStackTrace("", 99);
}
/** Logs a stack trace to a certain depth */
public static void logStackTrace(int depth) {
logStackTrace("", depth);
}
/** Logs a stack trace to a certain depth with a key */
public static void logStackTrace(String key, int depth) {
int offset = 0;
StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
String tinyStackTrace = "";
// Skip over the known stack trace classes
for (int i = 0; i < callStack.length; i++) {
StackTraceElement el = callStack[i];
String className = el.getClassName();
if (className.indexOf("dalvik.system.VMStack") == -1 &&
className.indexOf("java.lang.Thread") == -1 &&
className.indexOf("recents.Console") == -1) {
break;
} else {
offset++;
}
}
// Build the pretty stack trace
int start = Math.min(offset + depth, callStack.length);
int end = offset;
String indent = "";
for (int i = start - 1; i >= end; i--) {
StackTraceElement el = callStack[i];
tinyStackTrace += indent + " -> " + el.getClassName() +
"[" + el.getLineNumber() + "]." + el.getMethodName();
if (i > end) {
tinyStackTrace += "\n";
indent += " ";
}
}
log(true, key, tinyStackTrace, AnsiRed);
}
/** Returns the stringified MotionEvent action */
public static String motionEventActionToString(int action) {
switch (action) {
@@ -93,4 +138,25 @@ public class Console {
return "" + action;
}
}
public static String trimMemoryLevelToString(int level) {
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
return "UI Hidden";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
return "Running Moderate";
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
return "Background";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
return "Running Low";
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
return "Moderate";
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
return "Critical";
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
return "Complete";
default:
return "" + level;
}
}
}

View File

@@ -30,9 +30,11 @@ public class Constants {
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 final boolean Memory = false;
}
public static class UI {
@@ -41,7 +43,7 @@ public class Constants {
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 final boolean HwLayers = false;
}
public static class TaskStack {
@@ -55,13 +57,16 @@ public class Constants {
public static class Values {
public static class Window {
// The dark background dim is set behind the empty recents view
public static final float DarkBackgroundDim = 0.5f;
// The background dim is set behind the card stack
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;
// For debugging, this allows us to multiply the number of cards for each task
public static final int TaskEntryMultiplier = 1;
}

View File

@@ -23,6 +23,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.android.systemui.recent.RecentTasksLoader;
import com.android.systemui.recents.model.SpaceNode;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.RecentsView;
@@ -167,6 +168,14 @@ public class RecentsActivity extends Activity {
mVisible = false;
}
@Override
public void onTrimMemory(int level) {
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
if (loader != null) {
loader.onTrimMemory(level);
}
}
@Override
public void onBackPressed() {
if (!mRecentsView.unfilterFilteredStacks()) {

View File

@@ -30,7 +30,6 @@ public class RecentsConfiguration {
DisplayMetrics mDisplayMetrics;
public boolean layoutVerticalStack;
public Rect systemInsets = new Rect();
/** Private constructor */
@@ -56,7 +55,6 @@ public class RecentsConfiguration {
boolean isPortrait = context.getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_PORTRAIT;
layoutVerticalStack = isPortrait || Constants.LANDSCAPE_LAYOUT_VERTICAL_STACK;
}
public void updateSystemInsets(Rect insets) {

View File

@@ -57,8 +57,9 @@ public class RecentsService extends Service {
tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top);
tsv.boundScroll();
TaskViewTransform transform = tsv.getStackTransform(0);
Rect taskRect = new Rect(transform.rect);
data.putParcelable("taskRect", transform.rect);
data.putParcelable("taskRect", taskRect);
Message reply = Message.obtain(null, MSG_UPDATE_RECENTS_FOR_CONFIGURATION, 0, 0);
reply.setData(data);
msg.replyTo.send(reply);
@@ -100,4 +101,12 @@ public class RecentsService extends Service {
Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onDestroy]");
super.onDestroy();
}
@Override
public void onTrimMemory(int level) {
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
if (loader != null) {
loader.onTrimMemory(level);
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.recents;
import android.app.ActivityManager;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
@@ -35,6 +36,7 @@ import com.android.systemui.recents.model.TaskStack;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -233,7 +235,7 @@ class BitmapLruCache extends LruCache<Task, Bitmap> {
@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;
return bitmap.getAllocationByteCount() / 1024;
}
}
@@ -247,17 +249,28 @@ public class RecentsTaskLoader {
TaskResourceLoadQueue mLoadQueue;
TaskResourceLoader mLoader;
int mMaxThumbnailCacheSize;
int mMaxIconCacheSize;
BitmapDrawable mDefaultIcon;
Bitmap mDefaultThumbnail;
/** Private Constructor */
private RecentsTaskLoader(Context context) {
// Calculate the cache sizes
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,
mMaxThumbnailCacheSize = maxMemory / 8;
mMaxIconCacheSize = mMaxThumbnailCacheSize / 4;
int iconCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 :
mMaxIconCacheSize;
int thumbnailCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 :
mMaxThumbnailCacheSize;
Console.log(Constants.DebugFlags.App.EnableBackgroundTaskLoading,
"[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize +
" iconCache: " + iconCacheSize);
// Initialize the cache and loaders
mLoadQueue = new TaskResourceLoadQueue();
mIconCache = new DrawableLruCache(iconCacheSize);
mThumbnailCache = new BitmapLruCache(thumbnailCacheSize);
@@ -293,7 +306,7 @@ public class RecentsTaskLoader {
/** Reload the set of recent tasks */
SpaceNode reload(Context context, int preloadCount) {
Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|reload]");
Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|reload]");
TaskStack stack = new TaskStack(context);
SpaceNode root = new SpaceNode(context);
root.setStack(stack);
@@ -310,7 +323,7 @@ public class RecentsTaskLoader {
Console.log(Constants.DebugFlags.App.TimeSystemCalls,
"[RecentsTaskLoader|getRecentTasks]",
"" + (System.currentTimeMillis() - t1) + "ms");
Console.log(Constants.DebugFlags.App.SystemUIHandshake,
Console.log(Constants.DebugFlags.App.TaskDataLoader,
"[RecentsTaskLoader|tasks]", "" + tasks.size());
// Remove home/recents tasks
@@ -335,35 +348,51 @@ public class RecentsTaskLoader {
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,
// Preload the specified number of apps
if (i >= (taskCount - preloadCount) ||
!Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
Console.log(Constants.DebugFlags.App.TaskDataLoader,
"[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);
task = new Task(t.persistentId, t.baseIntent, title, null, null);
// Load the icon (if possible from the cache)
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
task.icon = mIconCache.get(task);
}
if (task.icon == null) {
task.icon = info.loadIcon(pm);
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
if (thumbnail != null) mThumbnailCache.put(task, thumbnail);
if (icon != null) {
mIconCache.put(task, icon);
}
mIconCache.put(task, task.icon);
}
}
// Load the thumbnail (if possible from the cache)
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
task.thumbnail = mThumbnailCache.get(task);
}
if (task.thumbnail == null) {
task.thumbnail = am.getTaskTopThumbnail(t.id);
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
mThumbnailCache.put(task, task.thumbnail);
}
}
// Create as many tasks a we want to multiply by
for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) {
Console.log(Constants.DebugFlags.App.TaskDataLoader,
" [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName());
stack.addTask(task);
}
} else {
// Create as many tasks a we want to multiply by
for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) {
Console.log(Constants.DebugFlags.App.SystemUIHandshake,
Console.log(Constants.DebugFlags.App.TaskDataLoader,
" [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName());
task = new Task(t.persistentId, t.baseIntent, title, null, null);
stack.addTask(task);
@@ -388,9 +417,9 @@ public class RecentsTaskLoader {
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());
Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stacks]", "" + tasks.size());
for (ActivityManager.StackInfo s : stackInfos) {
Console.log(Constants.DebugFlags.App.SystemUIHandshake, " [RecentsTaskLoader|stack]", s.toString());
Console.log(Constants.DebugFlags.App.TaskDataLoader, " [RecentsTaskLoader|stack]", s.toString());
if (stacks.containsKey(s.stackId)) {
stacks.get(s.stackId).setRect(s.bounds);
}
@@ -403,45 +432,46 @@ public class RecentsTaskLoader {
return root;
}
/** Acquires the task resource data from the pool.
* XXX: Move this into Task? */
/** Acquires the task resource data from the pool. */
public void loadTaskData(Task t) {
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
t.icon = mIconCache.get(t);
t.thumbnail = mThumbnailCache.get(t);
Drawable icon = mIconCache.get(t);
Bitmap thumbnail = mThumbnailCache.get(t);
Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]",
t + " icon: " + t.icon + " thumbnail: " + t.thumbnail);
t + " icon: " + icon + " thumbnail: " + thumbnail +
" thumbnailCacheSize: " + mThumbnailCache.size());
boolean requiresLoad = false;
if (t.icon == null) {
t.icon = mDefaultIcon;
if (icon == null) {
icon = mDefaultIcon;
requiresLoad = true;
}
if (t.thumbnail == null) {
t.thumbnail = mDefaultThumbnail;
if (thumbnail == null) {
thumbnail = mDefaultThumbnail;
requiresLoad = true;
}
if (requiresLoad) {
mLoadQueue.addTask(t);
}
t.notifyTaskLoaded(thumbnail, icon);
}
}
/** Releases the task resource data back into the pool.
* XXX: Move this into Task? */
/** Releases the task resource data back into the pool. */
public void unloadTaskData(Task t) {
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
Console.log(Constants.DebugFlags.App.TaskDataLoader,
"[RecentsTaskLoader|unloadTask]", t);
"[RecentsTaskLoader|unloadTask]", t +
" thumbnailCacheSize: " + mThumbnailCache.size());
mLoadQueue.removeTask(t);
t.icon = mDefaultIcon;
t.thumbnail = mDefaultThumbnail;
t.notifyTaskUnloaded(mDefaultThumbnail, mDefaultIcon);
} else {
t.notifyTaskUnloaded(null, null);
}
}
/** Completely removes the resource data from the pool.
* XXX: Move this into Task? */
/** Completely removes the resource data from the pool. */
public void deleteTaskData(Task t) {
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
Console.log(Constants.DebugFlags.App.TaskDataLoader,
@@ -449,9 +479,10 @@ public class RecentsTaskLoader {
mLoadQueue.removeTask(t);
mThumbnailCache.remove(t);
mIconCache.remove(t);
t.notifyTaskUnloaded(mDefaultThumbnail, mDefaultIcon);
} else {
t.notifyTaskUnloaded(null, null);
}
t.icon = mDefaultIcon;
t.thumbnail = mDefaultThumbnail;
}
/** Stops the task loader */
@@ -460,4 +491,51 @@ public class RecentsTaskLoader {
mLoader.stop();
mLoadQueue.clearTasks();
}
void onTrimMemory(int level) {
Console.log(Constants.DebugFlags.App.Memory, "[RecentsTaskLoader|onTrimMemory]",
Console.trimMemoryLevelToString(level));
if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) {
// If we are hidden, then we should unload each of the task keys
if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
Console.log(Constants.DebugFlags.App.Memory, "[RecentsTaskLoader|unloadTasks]"
);
// Unload each of the keys in the thumbnail cache
Map<Task, Bitmap> thumbnailCache = mThumbnailCache.snapshot();
for (Task t : thumbnailCache.keySet()) {
unloadTaskData(t);
}
// As well as the keys in the icon cache
Map<Task, Drawable> iconCache = mIconCache.snapshot();
for (Task t : iconCache.keySet()) {
unloadTaskData(t);
}
}
switch (level) {
case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
// We are leaving recents, so trim the data a bit
mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2);
mIconCache.trimToSize(mMaxIconCacheSize / 2);
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
// We are going to be low on memory
mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 4);
mIconCache.trimToSize(mMaxIconCacheSize / 4);
break;
case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
// We are low on memory, so release everything
mThumbnailCache.evictAll();
mIconCache.evictAll();
break;
default:
break;
}
}
}
}

View File

@@ -54,6 +54,24 @@ public class Task {
}
}
/** Notifies the callback listeners that this task has been loaded */
public void notifyTaskLoaded(Bitmap thumbnail, Drawable icon) {
this.icon = icon;
this.thumbnail = thumbnail;
if (mCb != null) {
mCb.onTaskBound();
}
}
/** Notifies the callback listeners that this task has been unloaded */
public void notifyTaskUnloaded(Bitmap defaultThumbnail, Drawable defaultIcon) {
icon = defaultIcon;
thumbnail = defaultThumbnail;
if (mCb != null) {
mCb.onTaskUnbound();
}
}
@Override
public boolean equals(Object o) {
// If we have multiple task entries for the same task, then we do the simple object

View File

@@ -20,4 +20,8 @@ package com.android.systemui.recents.model;
public interface TaskCallbacks {
/* Notifies when a task's data has been updated */
public void onTaskDataChanged(Task task);
/* Notifies when a task has been bound */
public void onTaskBound();
/* Notifies when a task has been unbound */
public void onTaskUnbound();
}

View File

@@ -28,6 +28,8 @@ import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.animation.LinearInterpolator;
import com.android.systemui.recents.Console;
import com.android.systemui.recents.Constants;
/**
* This class facilitates swipe to dismiss. It defines an interface to be implemented by the
@@ -176,6 +178,9 @@ public class SwipeHelper {
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
Console.log(Constants.DebugFlags.UI.TouchEvents,
"[SwipeHelper|interceptTouchEvent]",
Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
final int action = ev.getAction();
switch (action) {
@@ -200,7 +205,7 @@ public class SwipeHelper {
if (Math.abs(delta) > mPagingTouchSlop) {
mCallback.onBeginDrag(mCurrView);
mDragging = true;
mInitialTouchPos = getPos(ev) - getTranslation(mCurrView);
mInitialTouchPos = pos - getTranslation(mCurrView);
}
}
break;
@@ -286,6 +291,10 @@ public class SwipeHelper {
}
public boolean onTouchEvent(MotionEvent ev) {
Console.log(Constants.DebugFlags.UI.TouchEvents,
"[SwipeHelper|touchEvent]",
Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
if (!mDragging) {
if (!onInterceptTouchEvent(ev)) {
return mCanCurrViewBeDimissed;

View File

@@ -435,19 +435,12 @@ public class TaskStackView extends FrameLayout implements TaskStackCallbacks, Ta
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);
}
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);
// Update the scroll bounds
updateMinMaxScroll(false);
@@ -589,7 +582,6 @@ public class TaskStackView extends FrameLayout implements TaskStackCallbacks, Ta
// 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);
@@ -610,7 +602,6 @@ public class TaskStackView extends FrameLayout implements TaskStackCallbacks, Ta
// 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;
@@ -678,14 +669,13 @@ public class TaskStackView extends FrameLayout implements TaskStackCallbacks, Ta
}
/* Handles touch events */
class TaskStackViewTouchHandler {
class TaskStackViewTouchHandler implements SwipeHelper.Callback {
static int INACTIVE_POINTER_ID = -1;
TaskStackView mSv;
VelocityTracker mVelocityTracker;
boolean mIsScrolling;
boolean mIsSwiping;
int mInitialMotionX, mInitialMotionY;
int mLastMotionX, mLastMotionY;
@@ -697,21 +687,24 @@ class TaskStackViewTouchHandler {
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;
// The page touch slop is used to calculate when we start swiping
float mPagingTouchSlop;
SwipeHelper mSwipeHelper;
boolean mInterceptedBySwipeHelper;
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;
mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
mSv = sv;
float densityScale = context.getResources().getDisplayMetrics().density;
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
mSwipeHelper.setMinAlpha(1f);
}
/** Velocity tracker helpers */
@@ -754,11 +747,18 @@ class TaskStackViewTouchHandler {
"[TaskStackViewTouchHandler|interceptTouchEvent]",
Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
// Return early if we have no children
boolean hasChildren = (mSv.getChildCount() > 0);
if (!hasChildren) {
return false;
}
// Pass through to swipe helper if we are swiping
mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
if (mInterceptedBySwipeHelper) {
return true;
}
boolean wasScrolling = !mSv.mScroller.isFinished() ||
(mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
int action = ev.getAction();
@@ -777,7 +777,8 @@ class TaskStackViewTouchHandler {
mVelocityTracker.addMovement(ev);
// Check if the scroller is finished yet
mIsScrolling = !mSv.mScroller.isFinished();
mIsSwiping = false;
// Enable HW layers
mSv.addHwLayersRefCount();
break;
}
case MotionEvent.ACTION_MOVE: {
@@ -786,25 +787,7 @@ class TaskStackViewTouchHandler {
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) {
if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
// Save the touch move info
mIsScrolling = true;
// Initialize the velocity tracker if necessary
@@ -815,8 +798,6 @@ class TaskStackViewTouchHandler {
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
// Enable HW layers
mSv.addHwLayersRefCount();
}
mLastMotionX = x;
@@ -829,16 +810,17 @@ class TaskStackViewTouchHandler {
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();
// Disable HW layers
mSv.decHwLayersRefCount();
break;
}
}
return wasScrolling || mIsScrolling || mIsSwiping;
return wasScrolling || mIsScrolling;
}
/** Handles touch events once we have intercepted them */
@@ -853,6 +835,11 @@ class TaskStackViewTouchHandler {
return false;
}
// Pass through to swipe helper if we are swiping
if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
return true;
}
// Update the velocity tracker
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
@@ -871,7 +858,6 @@ class TaskStackViewTouchHandler {
// 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) {
@@ -886,28 +872,7 @@ class TaskStackViewTouchHandler {
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 (!mIsScrolling) {
if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
mIsScrolling = true;
// Initialize the velocity tracker
@@ -927,8 +892,6 @@ class TaskStackViewTouchHandler {
if (mSv.isScrollOutOfBounds()) {
mVelocityTracker.clear();
}
} else if (mIsSwiping) {
mActiveTaskView.setTranslationX(mActiveTaskView.getTranslationX() + deltaX);
}
mLastMotionX = x;
mLastMotionY = y;
@@ -936,107 +899,33 @@ class TaskStackViewTouchHandler {
break;
}
case MotionEvent.ACTION_UP: {
if (mIsScrolling || mIsSwiping) {
final TaskView activeTv = mActiveTaskView;
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
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);
}
}
if (mIsScrolling && (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
@@ -1044,25 +933,14 @@ class TaskStackViewTouchHandler {
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);
}
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
@@ -1072,4 +950,72 @@ class TaskStackViewTouchHandler {
}
return true;
}
/**** SwipeHelper Implementation ****/
@Override
public View getChildAtPosition(MotionEvent ev) {
return findViewAtPoint((int) ev.getX(), (int) ev.getY());
}
@Override
public boolean canChildBeDismissed(View v) {
return true;
}
@Override
public void onBeginDrag(View v) {
// Enable HW layers
mSv.addHwLayersRefCount();
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
@Override
public void onChildDismissed(View v) {
TaskView tv = (TaskView) v;
Task task = tv.getTask();
Activity activity = (Activity) mSv.getContext();
// We have to disable the listener to ensure that we
// don't hit this again
tv.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(tv.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();
}
@Override
public void onSnapBackCompleted(View v) {
// Do Nothing
}
@Override
public void onDragCancelled(View v) {
// Disable HW layers
mSv.decHwLayersRefCount();
}
}

View File

@@ -255,13 +255,13 @@ public class TaskView extends FrameLayout implements View.OnClickListener, TaskC
}
/** Actually synchronizes the model data into the views */
void syncToTask() {
private void syncToTask() {
mThumbnailView.rebindToTask(mTask, false);
mIconView.rebindToTask(mTask, false);
}
/** Unset the task and callback */
void unbindFromTask() {
private void unbindFromTask() {
mTask.setCallbacks(null);
mThumbnailView.unbindFromTask();
mIconView.unbindFromTask();
@@ -357,16 +357,16 @@ public class TaskView extends FrameLayout implements View.OnClickListener, TaskC
/** 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);
}
/**** TaskCallbacks Implementation ****/
@Override
public void onTaskDataChanged(Task task) {
Console.log(Constants.DebugFlags.App.EnableBackgroundTaskLoading,
@@ -379,6 +379,16 @@ public class TaskView extends FrameLayout implements View.OnClickListener, TaskC
}
}
@Override
public void onTaskBound() {
syncToTask();
}
@Override
public void onTaskUnbound() {
unbindFromTask();
}
@Override
public void onClick(View v) {
mCb.onTaskIconClicked(this);