/* * 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.ValueAnimator; import android.content.ComponentName; import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.os.Bundle; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.RecentsConfiguration; import com.android.systemui.recents.misc.DozeTrigger; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.misc.Utilities; import com.android.systemui.recents.model.RecentsPackageMonitor; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskStack; import com.android.systemui.statusbar.DismissView; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; /* The visual representation of a task stack view */ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks, TaskView.TaskViewCallbacks, TaskStackViewScroller.TaskStackViewScrollerCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>, RecentsPackageMonitor.PackageCallbacks { /** The TaskView callbacks */ interface TaskStackViewCallbacks { public void onTaskViewClicked(TaskStackView stackView, TaskView tv, TaskStack stack, Task t, boolean lockToTask); public void onTaskViewAppInfoClicked(Task t); public void onTaskViewDismissed(Task t); public void onAllTaskViewsDismissed(ArrayList<Task> removedTasks); public void onTaskStackFilterTriggered(); public void onTaskStackUnfilterTriggered(); public void onTaskResize(Task t); } RecentsConfiguration mConfig; TaskStack mStack; TaskStackViewLayoutAlgorithm mLayoutAlgorithm; TaskStackViewFilterAlgorithm mFilterAlgorithm; TaskStackViewScroller mStackScroller; TaskStackViewTouchHandler mTouchHandler; TaskStackViewCallbacks mCb; ViewPool<TaskView, Task> mViewPool; ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<TaskViewTransform>(); DozeTrigger mUIDozeTrigger; DebugOverlayView mDebugOverlay; Rect mTaskStackBounds = new Rect(); DismissView mDismissAllButton; boolean mDismissAllButtonAnimating; int mFocusedTaskIndex = -1; int mPrevAccessibilityFocusedIndex = -1; // Optimizations int mStackViewsAnimationDuration; boolean mStackViewsDirty = true; boolean mStackViewsClipDirty = true; boolean mAwaitingFirstLayout = true; boolean mStartEnterAnimationRequestedAfterLayout; boolean mStartEnterAnimationCompleted; ViewAnimation.TaskViewEnterContext mStartEnterAnimationContext; int[] mTmpVisibleRange = new int[2]; float[] mTmpCoord = new float[2]; Matrix mTmpMatrix = new Matrix(); Rect mTmpRect = new Rect(); TaskViewTransform mTmpTransform = new TaskViewTransform(); HashMap<Task, TaskView> mTmpTaskViewMap = new HashMap<Task, TaskView>(); ArrayList<TaskView> mTaskViews = new ArrayList<TaskView>(); List<TaskView> mImmutableTaskViews = new ArrayList<TaskView>(); LayoutInflater mInflater; boolean mLayersDisabled; // A convenience update listener to request updating clipping of tasks ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { requestUpdateStackViewsClip(); } }; public TaskStackView(Context context, TaskStack stack) { super(context); // Set the stack first setStack(stack); mConfig = RecentsConfiguration.getInstance(); mViewPool = new ViewPool<TaskView, Task>(context, this); mInflater = LayoutInflater.from(context); mLayoutAlgorithm = new TaskStackViewLayoutAlgorithm(mConfig); mFilterAlgorithm = new TaskStackViewFilterAlgorithm(mConfig, this, mViewPool); mStackScroller = new TaskStackViewScroller(context, mConfig, mLayoutAlgorithm); mStackScroller.setCallbacks(this); mTouchHandler = new TaskStackViewTouchHandler(context, this, mConfig, mStackScroller); mUIDozeTrigger = new DozeTrigger(mConfig.taskBarDismissDozeDelaySeconds, new Runnable() { @Override public void run() { // Show the task bar dismiss buttons List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); tv.startNoUserInteractionAnimation(); } } }); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } /** Sets the callbacks */ void setCallbacks(TaskStackViewCallbacks cb) { mCb = cb; } /** Sets the task stack */ void setStack(TaskStack stack) { // Set the new stack mStack = stack; if (mStack != null) { mStack.setCallbacks(this); } // Layout again with the new stack requestLayout(); } /** Returns the task stack. */ TaskStack getStack() { return mStack; } /** Sets the debug overlay */ public void setDebugOverlay(DebugOverlayView overlay) { mDebugOverlay = overlay; } /** Updates the list of task views */ void updateTaskViewsList() { mTaskViews.clear(); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View v = getChildAt(i); if (v instanceof TaskView) { mTaskViews.add((TaskView) v); } } mImmutableTaskViews = Collections.unmodifiableList(mTaskViews); } /** Gets the list of task views */ List<TaskView> getTaskViews() { return mImmutableTaskViews; } /** Resets this TaskStackView for reuse. */ void reset() { // Reset the focused task resetFocusedTask(); // Return all the views to the pool List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = taskViewCount - 1; i >= 0; i--) { mViewPool.returnViewToPool(taskViews.get(i)); } // Mark each task view for relayout if (mViewPool != null) { Iterator<TaskView> iter = mViewPool.poolViewIterator(); if (iter != null) { while (iter.hasNext()) { TaskView tv = iter.next(); tv.reset(); } } } // Reset the stack state mStack.reset(); mStackViewsDirty = true; mStackViewsClipDirty = true; mAwaitingFirstLayout = true; mPrevAccessibilityFocusedIndex = -1; if (mUIDozeTrigger != null) { mUIDozeTrigger.stopDozing(); mUIDozeTrigger.resetTrigger(); } mStackScroller.reset(); } /** Requests that the views be synchronized with the model */ void requestSynchronizeStackViewsWithModel() { requestSynchronizeStackViewsWithModel(0); } void requestSynchronizeStackViewsWithModel(int duration) { if (!mStackViewsDirty) { invalidate(); mStackViewsDirty = true; } if (mAwaitingFirstLayout) { // Skip the animation if we are awaiting first layout mStackViewsAnimationDuration = 0; } else { mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); } } /** Requests that the views clipping be updated. */ void requestUpdateStackViewsClip() { if (!mStackViewsClipDirty) { invalidate(); mStackViewsClipDirty = true; } } /** Finds the child view given a specific task. */ public TaskView getChildViewForTask(Task t) { List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); if (tv.getTask() == t) { return tv; } } return null; } /** Returns the stack algorithm for this task stack. */ public TaskStackViewLayoutAlgorithm getStackAlgorithm() { return mLayoutAlgorithm; } /** * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. */ private boolean updateStackTransforms(ArrayList<TaskViewTransform> taskTransforms, ArrayList<Task> tasks, float stackScroll, int[] visibleRangeOut, boolean boundTranslationsToRect) { int taskTransformCount = taskTransforms.size(); int taskCount = tasks.size(); int frontMostVisibleIndex = -1; int backMostVisibleIndex = -1; // We can reuse the task transforms where possible to reduce object allocation if (taskTransformCount < taskCount) { // If there are less transforms than tasks, then add as many transforms as necessary for (int i = taskTransformCount; i < taskCount; i++) { taskTransforms.add(new TaskViewTransform()); } } else if (taskTransformCount > taskCount) { // If there are more transforms than tasks, then just subset the transform list taskTransforms.subList(0, taskCount); } // Update the stack transforms TaskViewTransform prevTransform = null; for (int i = taskCount - 1; i >= 0; i--) { TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(tasks.get(i), stackScroll, taskTransforms.get(i), prevTransform); if (transform.visible) { if (frontMostVisibleIndex < 0) { frontMostVisibleIndex = i; } backMostVisibleIndex = i; } else { if (backMostVisibleIndex != -1) { // We've reached the end of the visible range, so going down the rest of the // stack, we can just reset the transforms accordingly while (i >= 0) { taskTransforms.get(i).reset(); i--; } break; } } if (boundTranslationsToRect) { transform.translationY = Math.min(transform.translationY, mLayoutAlgorithm.mViewRect.bottom); } prevTransform = transform; } if (visibleRangeOut != null) { visibleRangeOut[0] = frontMostVisibleIndex; visibleRangeOut[1] = backMostVisibleIndex; } return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1; } /** Synchronizes the views with the model */ boolean synchronizeStackViewsWithModel() { if (mStackViewsDirty) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); // Get all the task transforms ArrayList<Task> tasks = mStack.getTasks(); float stackScroll = mStackScroller.getStackScroll(); int[] visibleRange = mTmpVisibleRange; boolean isValidVisibleRange = updateStackTransforms(mCurrentTaskTransforms, tasks, stackScroll, visibleRange, false); if (mDebugOverlay != null) { mDebugOverlay.setText("vis[" + visibleRange[1] + "-" + visibleRange[0] + "]"); } // Inflate and add the dismiss button if necessary if (Constants.DebugFlags.App.EnableDismissAll && mDismissAllButton == null) { mDismissAllButton = (DismissView) mInflater.inflate(R.layout.recents_dismiss_button, this, false); mDismissAllButton.setOnButtonClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mStack.removeAllTasks(); } }); addView(mDismissAllButton, 0); } // Return all the invisible children to the pool mTmpTaskViewMap.clear(); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); boolean reaquireAccessibilityFocus = false; for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); Task task = tv.getTask(); int taskIndex = mStack.indexOfTask(task); if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) { mTmpTaskViewMap.put(task, tv); } else { mViewPool.returnViewToPool(tv); reaquireAccessibilityFocus |= (i == mPrevAccessibilityFocusedIndex); // Hide the dismiss button if the front most task is invisible if (task == mStack.getFrontMostTask()) { hideDismissAllButton(null); } } } // Pick up all the newly visible children and update all the existing children for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) { Task task = tasks.get(i); TaskViewTransform transform = mCurrentTaskTransforms.get(i); TaskView tv = mTmpTaskViewMap.get(task); int taskIndex = mStack.indexOfTask(task); if (tv == null) { tv = mViewPool.pickUpViewFromPool(task, task); if (mLayersDisabled) { tv.disableLayersForOneFrame(); } if (mStackViewsAnimationDuration > 0) { // For items in the list, put them in start animating them from the // approriate ends of the list where they are expected to appear if (Float.compare(transform.p, 0f) <= 0) { mLayoutAlgorithm.getStackTransform(0f, 0f, mTmpTransform, null); } else { mLayoutAlgorithm.getStackTransform(1f, 0f, mTmpTransform, null); } tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0); } // If we show the front most task view then ensure that the dismiss button // is visible too. if (!mAwaitingFirstLayout && (task == mStack.getFrontMostTask())) { showDismissAllButton(); } } // Animate the task into place tv.updateViewPropertiesToTaskTransform(mCurrentTaskTransforms.get(taskIndex), mStackViewsAnimationDuration, mRequestUpdateClippingListener); // Request accessibility focus on the next view if we removed the task // that previously held accessibility focus if (reaquireAccessibilityFocus) { taskViews = getTaskViews(); taskViewCount = taskViews.size(); if (taskViewCount > 0 && ssp.isTouchExplorationEnabled() && mPrevAccessibilityFocusedIndex != -1) { TaskView atv = taskViews.get(taskViewCount - 1); int indexOfTask = mStack.indexOfTask(atv.getTask()); if (mPrevAccessibilityFocusedIndex != indexOfTask) { tv.requestAccessibilityFocus(); mPrevAccessibilityFocusedIndex = indexOfTask; } } } } // Reset the request-synchronize params mStackViewsAnimationDuration = 0; mStackViewsDirty = false; mStackViewsClipDirty = true; return true; } return false; } /** Updates the clip for each of the task views. */ void clipTaskViews() { // Update the clip on each task child List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount - 1; i++) { TaskView tv = taskViews.get(i); TaskView nextTv = null; TaskView tmpTv = null; int clipBottom = 0; if (tv.shouldClipViewInStack()) { // Find the next view to clip against int nextIndex = i; while (nextIndex < (taskViewCount - 1)) { tmpTv = taskViews.get(++nextIndex); if (tmpTv != null && tmpTv.shouldClipViewInStack()) { nextTv = tmpTv; break; } } // Clip against the next view, this is just an approximation since we are // stacked and we can make assumptions about the visibility of the this // task relative to the ones in front of it. if (nextTv != null) { // Map the top edge of next task view into the local space of the current // task view to find the clip amount in local space mTmpCoord[0] = mTmpCoord[1] = 0; Utilities.mapCoordInDescendentToSelf(nextTv, this, mTmpCoord, false); Utilities.mapCoordInSelfToDescendent(tv, this, mTmpCoord, mTmpMatrix); clipBottom = (int) Math.floor(tv.getMeasuredHeight() - mTmpCoord[1] - nextTv.getPaddingTop() - 1); } } tv.getViewBounds().setClipBottom(clipBottom); } if (taskViewCount > 0) { // The front most task should never be clipped TaskView tv = taskViews.get(taskViewCount - 1); tv.getViewBounds().setClipBottom(0); } mStackViewsClipDirty = false; } /** The stack insets to apply to the stack contents */ public void setStackInsetRect(Rect r) { mTaskStackBounds.set(r); } /** Updates the min and max virtual scroll bounds */ void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab, boolean launchedFromHome) { // Compute the min and max scroll values mLayoutAlgorithm.computeMinMaxScroll(mStack.getTasks(), launchedWithAltTab, launchedFromHome); // Debug logging if (boundScrollToNewMinMax) { mStackScroller.boundScroll(); } } /** Returns the scroller. */ public TaskStackViewScroller getScroller() { return mStackScroller; } /** Focuses the task at the specified index in the stack */ void focusTask(int taskIndex, boolean scrollToNewPosition, final boolean animateFocusedState) { // Return early if the task is already focused if (taskIndex == mFocusedTaskIndex) return; if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) { mFocusedTaskIndex = taskIndex; mPrevAccessibilityFocusedIndex = taskIndex; // Focus the view if possible, otherwise, focus the view after we scroll into position final Task t = mStack.getTasks().get(mFocusedTaskIndex); Runnable postScrollRunnable = new Runnable() { @Override public void run() { TaskView tv = getChildViewForTask(t); if (tv != null) { tv.setFocusedTask(animateFocusedState); tv.requestAccessibilityFocus(); } } }; // Scroll the view into position (just center it in the curve) if (scrollToNewPosition) { float newScroll = mLayoutAlgorithm.getStackScrollForTask(t) - 0.5f; newScroll = mStackScroller.getBoundedStackScroll(newScroll); mStackScroller.animateScroll(mStackScroller.getStackScroll(), newScroll, postScrollRunnable); } else { if (postScrollRunnable != null) { postScrollRunnable.run(); } } } } /** * Ensures that there is a task focused, if nothing is focused, then we will use the task * at the center of the visible stack. */ public boolean ensureFocusedTask(boolean findClosestToCenter) { if (mFocusedTaskIndex < 0) { List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); if (findClosestToCenter) { // If there is no task focused, then find the task that is closes to the center // of the screen and use that as the currently focused task int x = mLayoutAlgorithm.mStackVisibleRect.centerX(); int y = mLayoutAlgorithm.mStackVisibleRect.centerY(); for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); tv.getHitRect(mTmpRect); if (mTmpRect.contains(x, y)) { mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); mPrevAccessibilityFocusedIndex = mFocusedTaskIndex; break; } } } // If we can't find the center task, then use the front most index if (mFocusedTaskIndex < 0 && taskViewCount > 0) { TaskView tv = taskViews.get(taskViewCount - 1); mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); mPrevAccessibilityFocusedIndex = mFocusedTaskIndex; } } return mFocusedTaskIndex >= 0; } /** * Focuses the next task in the stack. * @param animateFocusedState determines whether to actually draw the highlight along with * the change in focus, as well as whether to scroll to fit the * task into view. */ public void focusNextTask(boolean forward, boolean animateFocusedState) { // Find the next index to focus int numTasks = mStack.getTaskCount(); if (numTasks == 0) return; int direction = (forward ? -1 : 1); int newIndex = mFocusedTaskIndex + direction; if (newIndex >= 0 && newIndex <= (numTasks - 1)) { newIndex = Math.max(0, Math.min(numTasks - 1, newIndex)); focusTask(newIndex, true, animateFocusedState); } } /** Dismisses the focused task. */ public void dismissFocusedTask() { // Return early if the focused task index is invalid if (mFocusedTaskIndex < 0 || mFocusedTaskIndex >= mStack.getTaskCount()) { mFocusedTaskIndex = -1; return; } Task t = mStack.getTasks().get(mFocusedTaskIndex); TaskView tv = getChildViewForTask(t); tv.dismissTask(); } /** Resets the focused task. */ void resetFocusedTask() { if ((0 <= mFocusedTaskIndex) && (mFocusedTaskIndex < mStack.getTaskCount())) { Task t = mStack.getTasks().get(mFocusedTaskIndex); TaskView tv = getChildViewForTask(t); if (tv != null) { tv.unsetFocusedTask(); } } mFocusedTaskIndex = -1; mPrevAccessibilityFocusedIndex = -1; } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); if (taskViewCount > 0) { TaskView backMostTask = taskViews.get(0); TaskView frontMostTask = taskViews.get(taskViewCount - 1); event.setFromIndex(mStack.indexOfTask(backMostTask.getTask())); event.setToIndex(mStack.indexOfTask(frontMostTask.getTask())); event.setContentDescription(frontMostTask.getTask().activityLabel); } event.setItemCount(mStack.getTaskCount()); event.setScrollY(mStackScroller.mScroller.getCurrY()); event.setMaxScrollY(mStackScroller.progressToScrollRange(mLayoutAlgorithm.mMaxScrollP)); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); if (taskViewCount > 1 && mPrevAccessibilityFocusedIndex != -1) { info.setScrollable(true); if (mPrevAccessibilityFocusedIndex > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } if (mPrevAccessibilityFocusedIndex < mStack.getTaskCount() - 1) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } } } @Override public CharSequence getAccessibilityClassName() { return TaskStackView.class.getName(); } @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (super.performAccessibilityAction(action, arguments)) { return true; } if (ensureFocusedTask(false)) { switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { if (mPrevAccessibilityFocusedIndex > 0) { focusNextTask(true, false); return true; } } break; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { if (mPrevAccessibilityFocusedIndex < mStack.getTaskCount() - 1) { focusNextTask(false, false); return true; } } break; } } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mTouchHandler.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { return mTouchHandler.onTouchEvent(ev); } @Override public boolean onGenericMotionEvent(MotionEvent ev) { return mTouchHandler.onGenericMotionEvent(ev); } /** Returns the region that touch gestures can be started in. */ Rect getTouchableRegion() { return mTaskStackBounds; } @Override public void computeScroll() { mStackScroller.computeScroll(); // Synchronize the views synchronizeStackViewsWithModel(); clipTaskViews(); updateDismissButtonPosition(); // Notify accessibility sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); } /** Computes the stack and task rects */ public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds, boolean launchedWithAltTab, boolean launchedFromHome) { // Compute the rects in the stack algorithm mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds); // Update the scroll bounds updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome); } /** * This is ONLY used from AlternateRecentsComponent to update the dummy stack view for purposes * of getting the task rect to animate to. */ public void updateMinMaxScrollForStack(TaskStack stack, boolean launchedWithAltTab, boolean launchedFromHome) { mStack = stack; updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome); } /** * Computes the maximum number of visible tasks and thumbnails. Requires that * updateMinMaxScrollForStack() is called first. */ public TaskStackViewLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { return mLayoutAlgorithm.computeStackVisibilityReport(mStack.getTasks()); } /** * This is called with the full window width and height to allow stack view children to * perform the full screen transition down. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); // Compute our stack/task rects Rect taskStackBounds = new Rect(mTaskStackBounds); taskStackBounds.bottom -= mConfig.systemInsets.bottom; computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // If this is the first layout, then scroll to the front of the stack and synchronize the // stack views immediately to load all the views if (mAwaitingFirstLayout) { mStackScroller.setStackScrollToInitialState(); requestSynchronizeStackViewsWithModel(); synchronizeStackViewsWithModel(); } // Measure each of the TaskViews List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { mTmpRect.setEmpty(); } tv.measure( MeasureSpec.makeMeasureSpec( mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom, MeasureSpec.EXACTLY)); } // Measure the dismiss button if (mDismissAllButton != null) { int taskRectWidth = mLayoutAlgorithm.mTaskRect.width(); mDismissAllButton.measure( MeasureSpec.makeMeasureSpec(taskRectWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mConfig.dismissAllButtonSizePx, MeasureSpec.EXACTLY)); } setMeasuredDimension(width, height); } /** * This is called with the size of the space not including the top or right insets, or the * search bar height in portrait (but including the search bar width in landscape, since we want * to draw under it. */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // Layout each of the children List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { mTmpRect.setEmpty(); } tv.layout(mLayoutAlgorithm.mTaskRect.left - mTmpRect.left, mLayoutAlgorithm.mTaskRect.top - mTmpRect.top, mLayoutAlgorithm.mTaskRect.right + mTmpRect.right, mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom); } // Layout the dismiss button at the top of the screen, and just translate it accordingly // when synchronizing the views with the model to attach it to the bottom of the front-most // task view if (mDismissAllButton != null) { mDismissAllButton.layout(mLayoutAlgorithm.mTaskRect.left, 0, mLayoutAlgorithm.mTaskRect.left + mDismissAllButton.getMeasuredWidth(), mDismissAllButton.getMeasuredHeight()); } if (mAwaitingFirstLayout) { mAwaitingFirstLayout = false; onFirstLayout(); } } /** Handler for the first layout. */ void onFirstLayout() { int offscreenY = mLayoutAlgorithm.mViewRect.bottom - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top); // Find the launch target task Task launchTargetTask = null; List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); Task task = tv.getTask(); if (task.isLaunchTarget) { launchTargetTask = task; break; } } // Prepare the first view for its enter animation for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); Task task = tv.getTask(); boolean occludesLaunchTarget = (launchTargetTask != null) && launchTargetTask.group.isTaskAboveTask(task, launchTargetTask); tv.prepareEnterRecentsAnimation(task.isLaunchTarget, occludesLaunchTarget, offscreenY); } // If the enter animation started already and we haven't completed a layout yet, do the // enter animation now if (mStartEnterAnimationRequestedAfterLayout) { startEnterRecentsAnimation(mStartEnterAnimationContext); mStartEnterAnimationRequestedAfterLayout = false; mStartEnterAnimationContext = null; } // When Alt-Tabbing, focus the previous task (but leave the animation until we finish the // enter animation). if (mConfig.launchedWithAltTab) { if (mConfig.launchedFromAppWithThumbnail) { focusTask(Math.max(0, mStack.getTaskCount() - 2), false, mConfig.launchedHasConfigurationChanged); } else { focusTask(Math.max(0, mStack.getTaskCount() - 1), false, mConfig.launchedHasConfigurationChanged); } } // Start dozing if (!mConfig.multiStackEnabled) { mUIDozeTrigger.startDozing(); } } /** Requests this task stacks to start it's enter-recents animation */ public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { // If we are still waiting to layout, then just defer until then if (mAwaitingFirstLayout) { mStartEnterAnimationRequestedAfterLayout = true; mStartEnterAnimationContext = ctx; return; } if (mStack.getTaskCount() > 0) { // Find the launch target task Task launchTargetTask = null; List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); Task task = tv.getTask(); if (task.isLaunchTarget) { launchTargetTask = task; break; } } // Animate all the task views into view for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); Task task = tv.getTask(); ctx.currentTaskTransform = new TaskViewTransform(); ctx.currentStackViewIndex = i; ctx.currentStackViewCount = taskViewCount; ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect; ctx.currentTaskOccludesLaunchTarget = (launchTargetTask != null) && launchTargetTask.group.isTaskAboveTask(task, launchTargetTask); ctx.updateListener = mRequestUpdateClippingListener; mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(), ctx.currentTaskTransform, null); tv.startEnterRecentsAnimation(ctx); } // Add a runnable to the post animation ref counter to clear all the views ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { @Override public void run() { mStartEnterAnimationCompleted = true; // Poke the dozer to restart the trigger after the animation completes mUIDozeTrigger.poke(); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); if (taskViewCount > 0) { // Focus the first view if accessibility is enabled if (ssp.isTouchExplorationEnabled()) { TaskView tv = taskViews.get(taskViewCount - 1); tv.requestAccessibilityFocus(); mPrevAccessibilityFocusedIndex = mStack.indexOfTask(tv.getTask()); } } // Start the focus animation when alt-tabbing ArrayList<Task> tasks = mStack.getTasks(); if (mConfig.launchedWithAltTab && !mConfig.launchedHasConfigurationChanged && 0 <= mFocusedTaskIndex && mFocusedTaskIndex < tasks.size()) { TaskView tv = getChildViewForTask(tasks.get(mFocusedTaskIndex)); if (tv != null) { tv.setFocusedTask(true); } } // Show the dismiss button showDismissAllButton(); } }); } } /** Requests this task stack to start it's exit-recents animation. */ public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { // Stop any scrolling mStackScroller.stopScroller(); mStackScroller.stopBoundScrollAnimation(); // Animate all the task views out of view ctx.offscreenTranslationY = mLayoutAlgorithm.mViewRect.bottom - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top); // Animate the dismiss-all button hideDismissAllButton(null); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView tv = taskViews.get(i); tv.startExitToHomeAnimation(ctx); } } /** Requests this task stack to start it's dismiss-all animation. */ public void startDismissAllAnimation(final Runnable postAnimationRunnable) { // Clear the focused task resetFocusedTask(); // Animate the dismiss-all button hideDismissAllButton(new Runnable() { @Override public void run() { List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); int count = 0; for (int i = taskViewCount - 1; i >= 0; i--) { TaskView tv = taskViews.get(i); tv.startDeleteTaskAnimation(i > 0 ? null : postAnimationRunnable, count * 50); count++; } } }); } /** Animates a task view in this stack as it launches. */ public void startLaunchTaskAnimation(TaskView tv, Runnable r, boolean lockToTask) { Task launchTargetTask = tv.getTask(); List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { TaskView t = taskViews.get(i); if (t == tv) { t.setClipViewInStack(false); t.startLaunchTaskAnimation(r, true, true, lockToTask); } else { boolean occludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(t.getTask(), launchTargetTask); t.startLaunchTaskAnimation(null, false, occludesLaunchTarget, lockToTask); } } } /** Shows the dismiss button */ void showDismissAllButton() { if (mDismissAllButton == null) return; if (mDismissAllButtonAnimating || mDismissAllButton.getVisibility() != View.VISIBLE || Float.compare(mDismissAllButton.getAlpha(), 0f) == 0) { mDismissAllButtonAnimating = true; mDismissAllButton.setVisibility(View.VISIBLE); mDismissAllButton.showClearButton(); mDismissAllButton.findViewById(R.id.dismiss_text).setAlpha(1f); mDismissAllButton.setAlpha(0f); mDismissAllButton.animate() .alpha(1f) .setDuration(250) .withEndAction(new Runnable() { @Override public void run() { mDismissAllButtonAnimating = false; } }) .start(); } } /** Hides the dismiss button */ void hideDismissAllButton(final Runnable postAnimRunnable) { if (mDismissAllButton == null) return; mDismissAllButtonAnimating = true; mDismissAllButton.animate() .alpha(0f) .setDuration(200) .withEndAction(new Runnable() { @Override public void run() { mDismissAllButtonAnimating = false; mDismissAllButton.setVisibility(View.GONE); if (postAnimRunnable != null) { postAnimRunnable.run(); } } }) .start(); } /** Updates the dismiss button position */ void updateDismissButtonPosition() { if (mDismissAllButton == null) return; // Update the position of the clear-all button to hang it off the first task view if (mStack.getTaskCount() > 0) { mTmpCoord[0] = mTmpCoord[1] = 0; TaskView tv = getChildViewForTask(mStack.getFrontMostTask()); TaskViewTransform transform = mCurrentTaskTransforms.get(mStack.getTaskCount() - 1); if (tv != null && transform.visible) { Utilities.mapCoordInDescendentToSelf(tv, this, mTmpCoord, false); mDismissAllButton.setTranslationY(mTmpCoord[1] + (tv.getScaleY() * tv.getHeight())); mDismissAllButton.setTranslationX(-(mLayoutAlgorithm.mStackRect.width() - transform.rect.width()) / 2f); } } } /** Final callback after Recents is finally hidden. */ void onRecentsHidden() { reset(); } public boolean isTransformedTouchPointInView(float x, float y, View child) { return isTransformedTouchPointInView(x, y, child, null); } /** Pokes the dozer on user interaction. */ void onUserInteraction() { // Poke the doze trigger if it is dozing mUIDozeTrigger.poke(); } @Override protected void dispatchDraw(Canvas canvas) { mLayersDisabled = false; super.dispatchDraw(canvas); } public void disableLayersForOneFrame() { mLayersDisabled = true; List<TaskView> taskViews = getTaskViews(); for (int i = 0; i < taskViews.size(); i++) { taskViews.get(i).disableLayersForOneFrame(); } } /**** TaskStackCallbacks Implementation ****/ @Override public void onStackTaskAdded(TaskStack stack, Task t) { requestSynchronizeStackViewsWithModel(); } @Override public void onStackTaskRemoved(TaskStack stack, Task removedTask, Task newFrontMostTask) { // 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 TaskView tv = getChildViewForTask(removedTask); if (tv != null) { mViewPool.returnViewToPool(tv); } // Get the stack scroll of the task to anchor to (since we are removing something, the front // most task will be our anchor task) Task anchorTask = null; float prevAnchorTaskScroll = 0; boolean pullStackForward = stack.getTaskCount() > 0; if (pullStackForward) { anchorTask = mStack.getFrontMostTask(); prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); } // Update the min/max scroll and animate other task views into their new positions updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // Offset the stack by as much as the anchor task would otherwise move back if (pullStackForward) { float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll)); mStackScroller.boundScroll(); } // Animate all the tasks into place requestSynchronizeStackViewsWithModel(200); // Update the new front most task if (newFrontMostTask != null) { TaskView frontTv = getChildViewForTask(newFrontMostTask); if (frontTv != null) { frontTv.onTaskBound(newFrontMostTask); frontTv.fadeInActionButton(0, mConfig.taskViewEnterFromAppDuration); } } // If there are no remaining tasks, then either unfilter the current stack, or just close // the activity if there are no filtered stacks if (mStack.getTaskCount() == 0) { boolean shouldFinishActivity = true; if (mStack.hasFilteredTasks()) { mStack.unfilterTasks(); shouldFinishActivity = (mStack.getTaskCount() == 0); } if (shouldFinishActivity) { mCb.onAllTaskViewsDismissed(null); } } else { // Fade the dismiss button back in showDismissAllButton(); } // Notify the callback that we've removed the task and it can clean up after it. Note, we // do this after onAllTaskViewsDismissed() is called, to allow the home activity to be // started before the call to remove the task. mCb.onTaskViewDismissed(removedTask); } @Override public void onStackAllTasksRemoved(TaskStack stack, final ArrayList<Task> removedTasks) { // Announce for accessibility String msg = getContext().getString(R.string.accessibility_recents_all_items_dismissed); announceForAccessibility(msg); startDismissAllAnimation(new Runnable() { @Override public void run() { // Notify that all tasks have been removed mCb.onAllTaskViewsDismissed(removedTasks); } }); } @Override public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks, Task filteredTask) { /* // Stash the scroll and filtered task for us to restore to when we unfilter mStashedScroll = getStackScroll(); // Calculate the current task transforms ArrayList<TaskViewTransform> curTaskTransforms = getStackTransforms(curTasks, getStackScroll(), null, true); // Update the task offsets mLayoutAlgorithm.updateTaskOffsets(mStack.getTasks()); // Scroll the item to the top of the stack (sans-peek) rect so that we can see it better updateMinMaxScroll(false); float overlapHeight = mLayoutAlgorithm.getTaskOverlapHeight(); setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight)); boundScrollRaw(); // Compute the transforms of the items in the new stack after setting the new scroll final ArrayList<Task> tasks = mStack.getTasks(); final ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(mStack.getTasks(), getStackScroll(), null, true); // Animate mFilterAlgorithm.startFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); // Notify any callbacks mCb.onTaskStackFilterTriggered(); */ } @Override public void onStackUnfiltered(TaskStack newStack, final ArrayList<Task> curTasks) { /* // Calculate the current task transforms final ArrayList<TaskViewTransform> curTaskTransforms = getStackTransforms(curTasks, getStackScroll(), null, true); // Update the task offsets mLayoutAlgorithm.updateTaskOffsets(mStack.getTasks()); // Restore the stashed scroll updateMinMaxScroll(false); setStackScrollRaw(mStashedScroll); boundScrollRaw(); // Compute the transforms of the items in the new stack after restoring the stashed scroll final ArrayList<Task> tasks = mStack.getTasks(); final ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(tasks, getStackScroll(), null, true); // Animate mFilterAlgorithm.startFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms); // Clear the saved vars mStashedScroll = 0; // Notify any callbacks mCb.onTaskStackUnfilterTriggered(); */ } /**** ViewPoolConsumer Implementation ****/ @Override public TaskView createView(Context context) { return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false); } @Override public void prepareViewToEnterPool(TaskView tv) { Task task = tv.getTask(); // Clear the accessibility focus for that view if (tv.isAccessibilityFocused()) { tv.clearAccessibilityFocus(); } // Report that this tasks's data is no longer being used RecentsTaskLoader.getInstance().unloadTaskData(task); // Detach the view from the hierarchy detachViewFromParent(tv); // Update the task views list after removing the task view updateTaskViewsList(); // Reset the view properties tv.resetViewProperties(); // Reset the clip state of the task view tv.setClipViewInStack(false); } @Override public void prepareViewToLeavePool(TaskView tv, Task task, boolean isNewView) { // It is possible for a view to be returned to the view pool before it is laid out, // which means that we will need to relayout the view when it is first used next. boolean requiresRelayout = tv.getWidth() <= 0 && !isNewView; // Rebind the task and request that this task's data be filled into the TaskView tv.onTaskBound(task); // Load the task data RecentsTaskLoader.getInstance().loadTaskData(task); // If the doze trigger has already fired, then update the state for this task view if (mConfig.multiStackEnabled || mUIDozeTrigger.hasTriggered()) { tv.setNoUserInteractionState(); } // If we've finished the start animation, then ensure we always enable the focus animations if (mStartEnterAnimationCompleted) { tv.enableFocusAnimations(); } // Find the index where this task should be placed in the stack int insertIndex = -1; int taskIndex = mStack.indexOfTask(task); if (taskIndex != -1) { List<TaskView> taskViews = getTaskViews(); int taskViewCount = taskViews.size(); for (int i = 0; i < taskViewCount; i++) { Task tvTask = taskViews.get(i).getTask(); if (taskIndex < mStack.indexOfTask(tvTask)) { // Offset by 1 if we have a dismiss-all button insertIndex = i + (Constants.DebugFlags.App.EnableDismissAll ? 1 : 0); break; } } } // Add/attach the view to the hierarchy if (isNewView) { addView(tv, insertIndex); } else { attachViewToParent(tv, insertIndex, tv.getLayoutParams()); if (requiresRelayout) { tv.requestLayout(); } } // Update the task views list after adding the new task view updateTaskViewsList(); // Set the new state for this view, including the callbacks and view clipping tv.setCallbacks(this); tv.setTouchEnabled(true); tv.setClipViewInStack(true); } @Override public boolean hasPreferredData(TaskView tv, Task preferredData) { return (tv.getTask() == preferredData); } /**** TaskViewCallbacks Implementation ****/ @Override public void onTaskViewAppIconClicked(TaskView tv) { if (Constants.DebugFlags.App.EnableTaskFiltering) { if (mStack.hasFilteredTasks()) { mStack.unfilterTasks(); } else { mStack.filterTasks(tv.getTask()); } } } @Override public void onTaskViewAppInfoClicked(TaskView tv) { if (mCb != null) { mCb.onTaskViewAppInfoClicked(tv.getTask()); // Keep track of app-info invocations MetricsLogger.count(getContext(), "overview_app_info", 1); } } @Override public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask) { // Cancel any doze triggers mUIDozeTrigger.stopDozing(); if (mCb != null) { mCb.onTaskViewClicked(this, tv, mStack, task, lockToTask); } } @Override public void onTaskViewDismissed(TaskView tv) { Task task = tv.getTask(); int taskIndex = mStack.indexOfTask(task); boolean taskWasFocused = tv.isFocusedTask(); // Announce for accessibility tv.announceForAccessibility(getContext().getString(R.string.accessibility_recents_item_dismissed, tv.getTask().activityLabel)); // Remove the task from the view mStack.removeTask(task); // If the dismissed task was focused, then we should focus the new task in the same index if (taskWasFocused) { ArrayList<Task> tasks = mStack.getTasks(); int nextTaskIndex = Math.min(tasks.size() - 1, taskIndex - 1); if (nextTaskIndex >= 0) { Task nextTask = tasks.get(nextTaskIndex); TaskView nextTv = getChildViewForTask(nextTask); if (nextTv != null) { // Focus the next task, and only animate the visible state if we are launched // from Alt-Tab nextTv.setFocusedTask(mConfig.launchedWithAltTab); } } } } @Override public void onTaskViewClipStateChanged(TaskView tv) { if (!mStackViewsDirty) { invalidate(); } } @Override public void onTaskViewFocusChanged(TaskView tv, boolean focused) { if (focused) { mFocusedTaskIndex = mStack.indexOfTask(tv.getTask()); } } @Override public void onTaskResize(TaskView tv) { if (mCb != null) { mCb.onTaskResize(tv.getTask()); } } /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/ @Override public void onScrollChanged(float p) { mUIDozeTrigger.poke(); requestSynchronizeStackViewsWithModel(); postInvalidateOnAnimation(); } /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ @Override public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) { // Compute which components need to be removed HashSet<ComponentName> removedComponents = monitor.computeComponentsRemoved( mStack.getTaskKeys(), packageName, userId); // For other tasks, just remove them directly if they no longer exist ArrayList<Task> tasks = mStack.getTasks(); for (int i = tasks.size() - 1; i >= 0; i--) { final Task t = tasks.get(i); if (removedComponents.contains(t.key.baseIntent.getComponent())) { TaskView tv = getChildViewForTask(t); if (tv != null) { // For visible children, defer removing the task until after the animation tv.startDeleteTaskAnimation(new Runnable() { @Override public void run() { mStack.removeTask(t); } }, 0); } else { // Otherwise, remove the task from the stack immediately mStack.removeTask(t); } } } } }