/* * Copyright (C) 2017 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.quickstep.views; import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.Utilities.squaredTouchSlop; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON; import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.SparseBooleanArray; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ListView; import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.SpringForce; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherState; import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PropertyListBuilder; import com.android.launcher3.anim.SpringObjectAnimator; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.RotationMode; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.OverScroller; import com.android.launcher3.util.PendingAnimation; import com.android.launcher3.util.Themes; import com.android.launcher3.util.ViewPool; import com.android.quickstep.RecentsAnimationWrapper; import com.android.quickstep.RecentsModel; import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener; import com.android.quickstep.TaskThumbnailCache; import com.android.quickstep.TaskUtils; import com.android.quickstep.util.ClipAnimationHelper; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.LauncherEventUtil; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; import com.android.systemui.shared.system.TaskStackChangeListener; import java.util.ArrayList; import java.util.function.Consumer; /** * A list of recent tasks. */ @TargetApi(Build.VERSION_CODES.P) public abstract class RecentsView extends PagedView implements Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener { private static final String TAG = RecentsView.class.getSimpleName(); public static final FloatProperty CONTENT_ALPHA = new FloatProperty("contentAlpha") { @Override public void setValue(RecentsView view, float v) { view.setContentAlpha(v); } @Override public Float get(RecentsView view) { return view.getContentAlpha(); } }; public static final FloatProperty FULLSCREEN_PROGRESS = new FloatProperty("fullscreenProgress") { @Override public void setValue(RecentsView recentsView, float v) { recentsView.setFullscreenProgress(v); } @Override public Float get(RecentsView recentsView) { return recentsView.mFullscreenProgress; } }; protected RecentsAnimationWrapper mRecentsAnimationWrapper; protected ClipAnimationHelper mClipAnimationHelper; protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier; protected int mTaskWidth; protected int mTaskHeight; protected boolean mEnableDrawingLiveTile = false; protected final Rect mTempRect = new Rect(); protected final RectF mTempRectF = new RectF(); private static final int DISMISS_TASK_DURATION = 300; private static final int ADDITION_TASK_DURATION = 200; // The threshold at which we update the SystemUI flags when animating from the task into the app public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; protected final T mActivity; private final float mFastFlingVelocity; private final RecentsModel mModel; private final int mTaskTopMargin; private final ClearAllButton mClearAllButton; private final Rect mClearAllButtonDeadZoneRect = new Rect(); private final Rect mTaskViewDeadZoneRect = new Rect(); protected final ClipAnimationHelper mTempClipAnimationHelper; private final ScrollState mScrollState = new ScrollState(); // Keeps track of the previously known visible tasks for purposes of loading/unloading task data private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); private final InvariantDeviceProfile mIdp; private final ViewPool mTaskViewPool; private boolean mDwbToastShown; private boolean mDisallowScrollToClearAll; private boolean mOverlayEnabled; private boolean mFreezeViewVisibility; /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { if (!mHandleTaskStackChanges) { return; } // Check this is for the right user if (!checkCurrentOrManagedUserId(userId, getContext())) { return; } // Remove the task immediately from the task list TaskView taskView = getTaskView(taskId); if (taskView != null) { removeView(taskView); } } @Override public void onActivityUnpinned() { if (!mHandleTaskStackChanges) { return; } reloadIfNeeded(); enableLayoutTransitions(); } @Override public void onTaskRemoved(int taskId) { if (!mHandleTaskStackChanges) { return; } BACKGROUND_EXECUTOR.execute(() -> { TaskView taskView = getTaskView(taskId); if (taskView == null) { return; } Handler handler = taskView.getHandler(); if (handler == null) { return; } // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and // remove all these checks Task.TaskKey taskKey = taskView.getTask().key; if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), taskKey.userId) == null) { // The package was uninstalled handler.post(() -> dismissTask(taskView, true /* animate */, false /* removeTask */)); } else { mModel.findTaskWithId(taskKey.id, (key) -> { if (key == null) { // The task was removed from the recents list handler.post(() -> dismissTask(taskView, true /* animate */, false /* removeTask */)); } }); } }); } @Override public void onPinnedStackAnimationStarted() { // Needed for activities that auto-enter PiP, which will not trigger a remote // animation to be created mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); } }; // Used to keep track of the last requested task list id, so that we do not request to load the // tasks again if we have already requested it and the task list has not changed private int mTaskListChangeId = -1; // Only valid until the launcher state changes to NORMAL protected int mRunningTaskId = -1; private boolean mRunningTaskTileHidden; private Task mTmpRunningTask; private boolean mRunningTaskIconScaledDown = false; private boolean mOverviewStateEnabled; private boolean mHandleTaskStackChanges; private boolean mSwipeDownShouldLaunchApp; private boolean mTouchDownToStartHome; private final float mSquaredTouchSlop; private int mDownX; private int mDownY; private PendingAnimation mPendingAnimation; private LayoutTransition mLayoutTransition; @ViewDebug.ExportedProperty(category = "launcher") private float mContentAlpha = 1; @ViewDebug.ExportedProperty(category = "launcher") protected float mFullscreenProgress = 0; // Keeps track of task id whose visual state should not be reset private int mIgnoreResetTaskId = -1; // Variables for empty state private final Drawable mEmptyIcon; private final CharSequence mEmptyMessage; private final TextPaint mEmptyMessagePaint; private final Point mLastMeasureSize = new Point(); private final int mEmptyMessagePadding; private boolean mShowEmptyMessage; private Layout mEmptyTextLayout; private LiveTileOverlay mLiveTileOverlay; private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = (inMultiWindowMode) -> { if (!inMultiWindowMode && mOverviewStateEnabled) { // TODO: Re-enable layout transitions for addition of the unpinned task reloadIfNeeded(); } }; public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); setEnableFreeScroll(true); mFastFlingVelocity = getResources() .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); mActivity = (T) BaseActivity.fromContext(context); mModel = RecentsModel.INSTANCE.get(context); mIdp = InvariantDeviceProfile.INSTANCE.get(context); mTempClipAnimationHelper = new ClipAnimationHelper(context); mClearAllButton = (ClearAllButton) LayoutInflater.from(context) .inflate(R.layout.overview_clear_all_button, this, false); mClearAllButton.setOnClickListener(this::dismissAllTasks); mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 10 /* initial size */); mIsRtl = !Utilities.isRtl(getResources()); setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); mTaskTopMargin = getResources() .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); mSquaredTouchSlop = squaredTouchSlop(context); mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); mEmptyIcon.setCallback(this); mEmptyMessage = context.getText(R.string.recents_empty_message); mEmptyMessagePaint = new TextPaint(); mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); mEmptyMessagePaint.setTextSize(getResources() .getDimension(R.dimen.recents_empty_message_text_size)); mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context), Typeface.NORMAL)); mEmptyMessagePadding = getResources() .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); setWillNotDraw(false); updateEmptyMessage(); // Initialize quickstep specific cache params here, as this is constructed only once mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5); } public OverScroller getScroller() { return mScroller; } public boolean isRtl() { return mIsRtl; } @Override public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { if (mHandleTaskStackChanges) { TaskView taskView = getTaskView(taskId); if (taskView != null) { Task task = taskView.getTask(); taskView.getThumbnail().setThumbnail(task, thumbnailData); return task; } } return null; } public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { TaskView taskView = getTaskView(taskId); if (taskView != null) { taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData); } return taskView; } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); updateTaskStackListenerState(); } @Override public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) { if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) == 0) { return; } mModel.getIconCache().clear(); reset(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().addCallback(this); mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); mSyncTransactionApplier = new SyncRtSurfaceTransactionApplierCompat(this); RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); mIdp.addOnChangeListener(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); updateTaskStackListenerState(); mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); mSyncTransactionApplier = null; RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); mIdp.removeOnChangeListener(this); } @Override public void onViewRemoved(View child) { super.onViewRemoved(child); // Clear the task data for the removed child if it was visible if (child != mClearAllButton) { TaskView taskView = (TaskView) child; mHasVisibleTaskData.delete(taskView.getTask().key.id); mTaskViewPool.recycle(taskView); } } public boolean isTaskViewVisible(TaskView tv) { // For now, just check if it's the active task or an adjacent task return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; } public TaskView getTaskView(int taskId) { for (int i = 0; i < getTaskViewCount(); i++) { TaskView tv = (TaskView) getChildAt(i); if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) { return tv; } } return null; } public void setOverviewStateEnabled(boolean enabled) { mOverviewStateEnabled = enabled; updateTaskStackListenerState(); } public void onDigitalWellbeingToastShown() { if (!mDwbToastShown) { mDwbToastShown = true; mActivity.getUserEventDispatcher().logActionTip( LauncherEventUtil.VISIBLE, LauncherLogProto.TipType.DWB_TOAST); } } @Override protected void onPageEndTransition() { super.onPageEndTransition(); if (getNextPage() > 0) { setSwipeDownShouldLaunchApp(true); } } @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); final int x = (int) ev.getX(); final int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_UP: if (mTouchDownToStartHome) { startHome(); } mTouchDownToStartHome = false; break; case MotionEvent.ACTION_CANCEL: mTouchDownToStartHome = false; break; case MotionEvent.ACTION_MOVE: // Passing the touch slop will not allow dismiss to home if (mTouchDownToStartHome && (isHandlingTouch() || squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) { mTouchDownToStartHome = false; } break; case MotionEvent.ACTION_DOWN: // Touch down anywhere but the deadzone around the visible clear all button and // between the task views will start home on touch up if (!isHandlingTouch()) { if (mShowEmptyMessage) { mTouchDownToStartHome = true; } else { updateDeadZoneRects(); final boolean clearAllButtonDeadZoneConsumed = mClearAllButton.getAlpha() == 1 && mClearAllButtonDeadZoneRect.contains(x, y); final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { mTouchDownToStartHome = true; } } } mDownX = x; mDownY = y; break; } // Do not let touch escape to siblings below this view. return isHandlingTouch() || shouldStealTouchFromSiblingsBelow(ev); } protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) { return true; } protected void applyLoadPlan(ArrayList tasks) { if (mPendingAnimation != null) { mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks)); return; } if (tasks == null || tasks.isEmpty()) { removeAllViews(); onTaskStackUpdated(); return; } int oldChildCount = getChildCount(); // Unload existing visible task data unloadVisibleTaskData(); TaskView ignoreRestTaskView = mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId); final int requiredTaskCount = tasks.size(); if (getTaskViewCount() != requiredTaskCount) { if (oldChildCount > 0) { removeView(mClearAllButton); } for (int i = getChildCount(); i < requiredTaskCount; i++) { addView(mTaskViewPool.getView()); } while (getChildCount() > requiredTaskCount) { removeView(getChildAt(getChildCount() - 1)); } if (requiredTaskCount > 0) { addView(mClearAllButton); } } // Rebind and reset all task views for (int i = requiredTaskCount - 1; i >= 0; i--) { final int pageIndex = requiredTaskCount - i - 1; final Task task = tasks.get(i); final TaskView taskView = (TaskView) getChildAt(pageIndex); taskView.bind(task); } TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { setCurrentPage(indexOfChild(runningTaskView)); } if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreRestTaskView) { // If the taskView mapping is changing, do not preserve the visuals. Since we are // mostly preserving the first task, and new taskViews are added to the end, it should // generally map to the same task. mIgnoreResetTaskId = -1; } resetTaskVisuals(); onTaskStackUpdated(); updateEnabledOverlays(); } public int getTaskViewCount() { // Account for the clear all button. int childCount = getChildCount(); return childCount == 0 ? 0 : childCount - 1; } protected void onTaskStackUpdated() { } public void resetTaskVisuals() { for (int i = getTaskViewCount() - 1; i >= 0; i--) { TaskView taskView = (TaskView) getChildAt(i); if (mIgnoreResetTaskId != taskView.getTask().key.id) { taskView.resetVisualProperties(); taskView.setStableAlpha(mContentAlpha); } } if (mRunningTaskTileHidden) { setRunningTaskHidden(mRunningTaskTileHidden); } // Force apply the scale. if (mIgnoreResetTaskId != mRunningTaskId) { applyRunningTaskIconScale(); } updateCurveProperties(); // Update the set of visible task's data loadVisibleTaskData(); } public void setFullscreenProgress(float fullscreenProgress) { mFullscreenProgress = fullscreenProgress; int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress); } } private void updateTaskStackListenerState() { boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() && getWindowVisibility() == VISIBLE; if (handleTaskStackChanges != mHandleTaskStackChanges) { mHandleTaskStackChanges = handleTaskStackChanges; if (handleTaskStackChanges) { reloadIfNeeded(); } } } @Override public void setInsets(Rect insets) { mInsets.set(insets); DeviceProfile dp = mActivity.getDeviceProfile(); getTaskSize(dp, mTempRect); mTaskWidth = mTempRect.width(); mTaskHeight = mTempRect.height(); mTempRect.top -= mTaskTopMargin; setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, dp.widthPx - mInsets.right - mTempRect.right, dp.heightPx - mInsets.bottom - mTempRect.bottom); } protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); public void getTaskSize(Rect outRect) { getTaskSize(mActivity.getDeviceProfile(), outRect); } @Override protected boolean computeScrollHelper() { boolean scrolling = super.computeScrollHelper(); boolean isFlingingFast = false; updateCurveProperties(); if (scrolling || isHandlingTouch()) { if (scrolling) { // Check if we are flinging quickly to disable high res thumbnail loading isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; } // After scrolling, update the visible task's data loadVisibleTaskData(); } // Update the high res thumbnail loader state mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast); return scrolling; } /** * Scales and adjusts translation of adjacent pages as if on a curved carousel. */ public void updateCurveProperties() { if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { return; } int scrollX = getScrollX(); final int halfPageWidth = getNormalChildWidth() / 2; final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth; final int halfScreenWidth = getMeasuredWidth() / 2; final int pageSpacing = mPageSpacing; mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX); final int pageCount = getPageCount(); for (int i = 0; i < pageCount; i++) { View page = getPageAt(i); float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; float distanceFromScreenCenter = screenCenter - pageCenter; float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; mScrollState.linearInterpolation = Math.min(1, Math.abs(distanceFromScreenCenter) / distanceToReachEdge); ((PageCallbacks) page).onPageScroll(mScrollState); } } /** * Iterates through all thet asks, and loads the associated task data for newly visible tasks, * and unloads the associated task data for tasks that are no longer visible. */ public void loadVisibleTaskData() { if (!mOverviewStateEnabled || mTaskListChangeId == -1) { // Skip loading visible task data if we've already left the overview state, or if the // task list hasn't been loaded yet (the task views will not reflect the task list) return; } int centerPageIndex = getPageNearestToCenterOfScreen(); int numChildren = getTaskViewCount(); int lower = Math.max(0, centerPageIndex - 2); int upper = Math.min(centerPageIndex + 2, numChildren - 1); // Update the task data for the in/visible children for (int i = 0; i < numChildren; i++) { TaskView taskView = (TaskView) getChildAt(i); Task task = taskView.getTask(); boolean visible = lower <= i && i <= upper; if (visible) { if (task == mTmpRunningTask) { // Skip loading if this is the task that we are animating into continue; } if (!mHasVisibleTaskData.get(task.key.id)) { taskView.onTaskListVisibilityChanged(true /* visible */); } mHasVisibleTaskData.put(task.key.id, visible); } else { if (mHasVisibleTaskData.get(task.key.id)) { taskView.onTaskListVisibilityChanged(false /* visible */); } mHasVisibleTaskData.delete(task.key.id); } } } /** * Unloads any associated data from the currently visible tasks */ private void unloadVisibleTaskData() { for (int i = 0; i < mHasVisibleTaskData.size(); i++) { if (mHasVisibleTaskData.valueAt(i)) { TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); if (taskView != null) { taskView.onTaskListVisibilityChanged(false /* visible */); } } } mHasVisibleTaskData.clear(); } @Override public void onHighResLoadingStateChanged(boolean enabled) { // Whenever the high res loading state changes, poke each of the visible tasks to see if // they want to updated their thumbnail state for (int i = 0; i < mHasVisibleTaskData.size(); i++) { if (mHasVisibleTaskData.valueAt(i)) { TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); if (taskView != null) { // Poke the view again, which will trigger it to load high res if the state // is enabled taskView.onTaskListVisibilityChanged(true /* visible */); } } } } public abstract void startHome(); public void reset() { setCurrentTask(-1); mIgnoreResetTaskId = -1; mTaskListChangeId = -1; mRecentsAnimationWrapper = null; mClipAnimationHelper = null; unloadVisibleTaskData(); setCurrentPage(0); mDwbToastShown = false; mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0); } public @Nullable TaskView getRunningTaskView() { return getTaskView(mRunningTaskId); } public int getRunningTaskIndex() { TaskView tv = getRunningTaskView(); return tv == null ? -1 : indexOfChild(tv); } /** * Reloads the view if anything in recents changed. */ public void reloadIfNeeded() { if (!mModel.isTaskListValid(mTaskListChangeId)) { mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); } } /** * Called when a gesture from an app is starting. */ public void onGestureAnimationStart(int runningTaskId) { // This needs to be called before the other states are set since it can create the task view showCurrentTask(runningTaskId); setEnableFreeScroll(false); setEnableDrawingLiveTile(false); setRunningTaskHidden(true); setRunningTaskIconScaledDown(true); } /** * Called only when a swipe-up gesture from an app has completed. Only called after * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}. */ public void onSwipeUpAnimationSuccess() { if (getRunningTaskView() != null) { float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() ? mLiveTileOverlay.cancelIconAnimation() : 0f; animateUpRunningTaskIconScale(startProgress); } setSwipeDownShouldLaunchApp(true); } /** * Called when a gesture from an app has finished. */ public void onGestureAnimationEnd() { setEnableFreeScroll(true); setEnableDrawingLiveTile(true); setOnScrollChangeListener(null); setRunningTaskViewShowScreenshot(true); setRunningTaskHidden(false); animateUpRunningTaskIconScale(); } /** * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}. * * All subsequent calls to reload will keep the task as the first item until {@link #reset()} * is called. Also scrolls the view to this task. */ public void showCurrentTask(int runningTaskId) { if (getTaskView(runningTaskId) == null) { boolean wasEmpty = getChildCount() == 0; // Add an empty view for now until the task plan is loaded and applied final TaskView taskView = mTaskViewPool.getView(); addView(taskView, 0); if (wasEmpty) { addView(mClearAllButton); } // The temporary running task is only used for the duration between the start of the // gesture and the task list is loaded and applied mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0, false, true, false, false, new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false); taskView.bind(mTmpRunningTask); } boolean runningTaskTileHidden = mRunningTaskTileHidden; setCurrentTask(runningTaskId); setCurrentPage(getRunningTaskIndex()); setRunningTaskViewShowScreenshot(false); setRunningTaskHidden(runningTaskTileHidden); // Reload the task list mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); } /** * Sets the running task id, cleaning up the old running task if necessary. * @param runningTaskId */ public void setCurrentTask(int runningTaskId) { if (mRunningTaskId == runningTaskId) { return; } if (mRunningTaskId != -1) { // Reset the state on the old running task view setRunningTaskIconScaledDown(false); setRunningTaskViewShowScreenshot(true); setRunningTaskHidden(false); } mRunningTaskId = runningTaskId; } /** * Hides the tile associated with {@link #mRunningTaskId} */ public void setRunningTaskHidden(boolean isHidden) { mRunningTaskTileHidden = isHidden; TaskView runningTask = getRunningTaskView(); if (runningTask != null) { runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha); } } private void setRunningTaskViewShowScreenshot(boolean showScreenshot) { if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { TaskView runningTaskView = getRunningTaskView(); if (runningTaskView != null) { runningTaskView.setShowScreenshot(showScreenshot); } } } public void showNextTask() { TaskView runningTaskView = getRunningTaskView(); if (runningTaskView == null) { // Launch the first task if (getTaskViewCount() > 0) { getTaskViewAt(0).launchTask(true /* animate */); } } else { TaskView nextTaskView = getNextTaskView(); if (nextTaskView != null) { nextTaskView.launchTask(true /* animate */); } else { runningTaskView.launchTask(true /* animate */); } } } public void setRunningTaskIconScaledDown(boolean isScaledDown) { if (mRunningTaskIconScaledDown != isScaledDown) { mRunningTaskIconScaledDown = isScaledDown; applyRunningTaskIconScale(); } } private void applyRunningTaskIconScale() { TaskView firstTask = getRunningTaskView(); if (firstTask != null) { firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1); } } public void animateUpRunningTaskIconScale() { animateUpRunningTaskIconScale(0); } public void animateUpRunningTaskIconScale(float startProgress) { mRunningTaskIconScaledDown = false; TaskView firstTask = getRunningTaskView(); if (firstTask != null) { firstTask.animateIconScaleAndDimIntoView(); firstTask.setIconScaleAnimStartProgress(startProgress); } } private void enableLayoutTransitions() { if (mLayoutTransition == null) { mLayoutTransition = new LayoutTransition(); mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING); mLayoutTransition.setDuration(ADDITION_TASK_DURATION); mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0); mLayoutTransition.addTransitionListener(new TransitionListener() { @Override public void startTransition(LayoutTransition transition, ViewGroup viewGroup, View view, int i) { } @Override public void endTransition(LayoutTransition transition, ViewGroup viewGroup, View view, int i) { // When the unpinned task is added, snap to first page and disable transitions if (view instanceof TaskView) { snapToPage(0); disableLayoutTransitions(); } } }); } setLayoutTransition(mLayoutTransition); } private void disableLayoutTransitions() { setLayoutTransition(null); } public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; } public boolean shouldSwipeDownLaunchApp() { return mSwipeDownShouldLaunchApp; } public interface PageCallbacks { /** * Updates the page UI based on scroll params. */ default void onPageScroll(ScrollState scrollState) {} } public static class ScrollState { /** * The progress from 0 to 1, where 0 is the center * of the screen and 1 is the edge of the screen. */ public float linearInterpolation; /** * The amount by which all the content is scrolled relative to the end of the list. */ public float scrollFromEdge; } public void setIgnoreResetTask(int taskId) { mIgnoreResetTaskId = taskId; } public void clearIgnoreResetTask(int taskId) { if (mIgnoreResetTaskId == taskId) { mIgnoreResetTaskId = -1; } } private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView) addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y, MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, SpringForce.STIFFNESS_MEDIUM, 0, -taskView.getHeight()), duration, LINEAR, anim); else { addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), duration, LINEAR, anim); } } private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, boolean shouldLog) { if (task != null) { ActivityManagerWrapper.getInstance().removeTask(task.key.id); if (shouldLog) { ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key); mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( onEndListener.logAction, Direction.UP, index, componentKey); mActivity.getStatsLogManager().logTaskDismiss(this, componentKey); } } } public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, boolean shouldRemoveTask, long duration) { if (mPendingAnimation != null) { mPendingAnimation.finish(false, Touch.SWIPE); } AnimatorSet anim = new AnimatorSet(); PendingAnimation pendingAnimation = new PendingAnimation(anim); int count = getPageCount(); if (count == 0) { return pendingAnimation; } int[] oldScroll = new int[count]; getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); int[] newScroll = new int[count]; getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); int taskCount = getTaskViewCount(); int scrollDiffPerPage = 0; if (count > 1) { scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); } int draggedIndex = indexOfChild(taskView); boolean needsCurveUpdates = false; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child == taskView) { if (animateTaskView) { addDismissedTaskAnimations(taskView, anim, duration); } } else { // If we just take newScroll - oldScroll, everything to the right of dragged task // translates to the left. We need to offset this in some cases: // - In RTL, add page offset to all pages, since we want pages to move to the right // Additionally, add a page offset if: // - Current page is rightmost page (leftmost for RTL) // - Dragging an adjacent page on the left side (right side for RTL) int offset = mIsRtl ? scrollDiffPerPage : 0; if (mCurrentPage == draggedIndex) { int lastPage = taskCount - 1; if (mCurrentPage == lastPage) { offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; } } else { // Dragging an adjacent page. int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) if (draggedIndex == negativeAdjacent) { offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; } } int scrollDiff = newScroll[i] - oldScroll[i] + offset; if (scrollDiff != 0) { if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) { addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X, MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, SpringForce.STIFFNESS_MEDIUM, 0, scrollDiff), duration, ACCEL, anim); } else { addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration, ACCEL, anim); } needsCurveUpdates = true; } } } if (needsCurveUpdates) { ValueAnimator va = ValueAnimator.ofFloat(0, 1); va.addUpdateListener((a) -> updateCurveProperties()); anim.play(va); } // Add a tiny bit of translation Z, so that it draws on top of other views if (animateTaskView) { taskView.setTranslationZ(0.1f); } mPendingAnimation = pendingAnimation; mPendingAnimation.addEndListener(new Consumer() { @Override public void accept(PendingAnimation.OnEndListener onEndListener) { if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask() && onEndListener.isSuccess) { finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener)); } else { onEnd(onEndListener); } } private void onEnd(PendingAnimation.OnEndListener onEndListener) { if (onEndListener.isSuccess) { if (shouldRemoveTask) { removeTask(taskView.getTask(), draggedIndex, onEndListener, true); } int pageToSnapTo = mCurrentPage; if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount() - 1)) { pageToSnapTo -= 1; } removeView(taskView); if (getTaskViewCount() == 0) { removeView(mClearAllButton); startHome(); } else { snapToPageImmediately(pageToSnapTo); } } resetTaskVisuals(); mPendingAnimation = null; } }); return pendingAnimation; } public PendingAnimation createAllTasksDismissAnimation(long duration) { if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } AnimatorSet anim = new AnimatorSet(); PendingAnimation pendingAnimation = new PendingAnimation(anim); int count = getTaskViewCount(); for (int i = 0; i < count; i++) { addDismissedTaskAnimations(getChildAt(i), anim, duration); } mPendingAnimation = pendingAnimation; mPendingAnimation.addEndListener((onEndListener) -> { if (onEndListener.isSuccess) { // Remove all the task views now ActivityManagerWrapper.getInstance().removeAllRecentTasks(); removeAllViews(); startHome(); } mPendingAnimation = null; }); return pendingAnimation; } private static void addAnim(Animator anim, long duration, TimeInterpolator interpolator, AnimatorSet set) { anim.setDuration(duration).setInterpolator(interpolator); set.play(anim); } private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { if (pageCount == 0) { return false; } final int newPageUnbound = getNextPage() + delta; if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { return false; } snapToPage((newPageUnbound + pageCount) % pageCount); getChildAt(getNextPage()).requestFocus(); return true; } private void runDismissAnimation(PendingAnimation pendingAnim) { AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( pendingAnim.anim, DISMISS_TASK_DURATION); controller.dispatchOnStart(); controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); controller.start(); } public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION)); } @SuppressWarnings("unused") private void dismissAllTasks(View view) { runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON); } private void dismissCurrentTask() { TaskView taskView = getTaskView(getNextPage()); if (taskView != null) { dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_TAB: return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, event.isAltPressed() /* cycle */); case KeyEvent.KEYCODE_DPAD_RIGHT: return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); case KeyEvent.KEYCODE_DPAD_LEFT: return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); case KeyEvent.KEYCODE_DEL: case KeyEvent.KEYCODE_FORWARD_DEL: dismissCurrentTask(); return true; case KeyEvent.KEYCODE_NUMPAD_DOT: if (event.isAltPressed()) { // Numpad DEL pressed while holding Alt. dismissCurrentTask(); return true; } } } return super.dispatchKeyEvent(event); } @Override protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (gainFocus && getChildCount() > 0) { switch (direction) { case FOCUS_FORWARD: setCurrentPage(0); break; case FOCUS_BACKWARD: case FOCUS_RIGHT: case FOCUS_LEFT: setCurrentPage(getChildCount() - 1); break; } } } public float getContentAlpha() { return mContentAlpha; } public void setContentAlpha(float alpha) { if (alpha == mContentAlpha) { return; } alpha = Utilities.boundToRange(alpha, 0, 1); mContentAlpha = alpha; for (int i = getTaskViewCount() - 1; i >= 0; i--) { TaskView child = getTaskViewAt(i); if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { child.setStableAlpha(alpha); } } mClearAllButton.setContentAlpha(mContentAlpha); int alphaInt = Math.round(alpha * 255); mEmptyMessagePaint.setAlpha(alphaInt); mEmptyIcon.setAlpha(alphaInt); if (alpha > 0) { setVisibility(VISIBLE); } else if (!mFreezeViewVisibility) { setVisibility(GONE); } } /** * Freezes the view visibility change. When frozen, the view will not change its visibility * to gone due to alpha changes. */ public void setFreezeViewVisibility(boolean freezeViewVisibility) { if (mFreezeViewVisibility != freezeViewVisibility) { mFreezeViewVisibility = freezeViewVisibility; if (!mFreezeViewVisibility) { setVisibility(mContentAlpha > 0 ? VISIBLE : GONE); } } } @Override public void onViewAdded(View child) { super.onViewAdded(child); child.setAlpha(mContentAlpha); } /** * @return The most recent task that is older than the currently running task. If there is * currently no running task or there is no task older than it, then return null. */ @Nullable public TaskView getNextTaskView() { TaskView runningTaskView = getRunningTaskView(); if (runningTaskView == null) { return null; } return getTaskViewAt(indexOfChild(runningTaskView) + 1); } public TaskView getTaskViewAt(int index) { View child = getChildAt(index); return child == mClearAllButton ? null : (TaskView) child; } public void updateEmptyMessage() { boolean isEmpty = getChildCount() == 0; boolean hasSizeChanged = mLastMeasureSize.x != getWidth() || mLastMeasureSize.y != getHeight(); if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { return; } setContentDescription(isEmpty ? mEmptyMessage : ""); mShowEmptyMessage = isEmpty; updateEmptyStateUi(hasSizeChanged); invalidate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); updateEmptyStateUi(changed); // Set the pivot points to match the task preview center setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); setPivotX(((mInsets.left + getPaddingLeft()) + (getWidth() - mInsets.right - getPaddingRight())) / 2); } private void updateDeadZoneRects() { // Get the deadzone rect surrounding the clear all button to not dismiss overview to home mClearAllButtonDeadZoneRect.setEmpty(); if (mClearAllButton.getWidth() > 0) { int verticalMargin = getResources() .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); } // Get the deadzone rect between the task views mTaskViewDeadZoneRect.setEmpty(); int count = getTaskViewCount(); if (count > 0) { final View taskView = getTaskViewAt(0); getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), taskView.getBottom()); } } private void updateEmptyStateUi(boolean sizeChanged) { boolean hasValidSize = getWidth() > 0 && getHeight() > 0; if (sizeChanged && hasValidSize) { mEmptyTextLayout = null; mLastMeasureSize.set(getWidth(), getHeight()); } if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), mEmptyMessagePaint, availableWidth) .setAlignment(Layout.Alignment.ALIGN_CENTER) .build(); int totalHeight = mEmptyTextLayout.getHeight() + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); int top = (mLastMeasureSize.y - totalHeight) / 2; int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), top + mEmptyIcon.getIntrinsicHeight()); } } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); } protected void maybeDrawEmptyMessage(Canvas canvas) { if (mShowEmptyMessage && mEmptyTextLayout != null) { // Offset to center in the visible (non-padded) part of RecentsView mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); canvas.save(); canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, (mTempRect.top - mTempRect.bottom) / 2); mEmptyIcon.draw(canvas); canvas.translate(mEmptyMessagePadding, mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); mEmptyTextLayout.draw(canvas); canvas.restore(); } } /** * Animate adjacent tasks off screen while scaling up. * * If launching one of the adjacent tasks, parallax the center task and other adjacent task * to the right. */ public AnimatorSet createAdjacentPageAnimForTaskLaunch( TaskView tv, ClipAnimationHelper clipAnimationHelper) { AnimatorSet anim = new AnimatorSet(); int taskIndex = indexOfChild(tv); int centerTaskIndex = getCurrentPage(); boolean launchingCenterTask = taskIndex == centerTaskIndex; LauncherState.ScaleAndTranslation toScaleAndTranslation = clipAnimationHelper .getScaleAndTranslation(); float toScale = toScaleAndTranslation.scale; float toTranslationY = toScaleAndTranslation.translationY; if (launchingCenterTask) { RecentsView recentsView = tv.getRecentsView(); anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale)); anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY)); anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1)); } else { // We are launching an adjacent task, so parallax the center and other adjacent task. float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, mIsRtl ? -displacementX : displacementX)); int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { anim.play(new PropertyListBuilder() .translationX(mIsRtl ? -displacementX : displacementX) .scale(1) .build(getPageAt(otherAdjacentTaskIndex))); } } return anim; } public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } int count = getChildCount(); if (count == 0) { return new PendingAnimation(new AnimatorSet()); } int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); final boolean[] passedOverviewThreshold = new boolean[] {false}; ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1); progressAnim.setInterpolator(LINEAR); progressAnim.addUpdateListener(animator -> { // Once we pass a certain threshold, update the sysui flags to match the target // tasks' flags mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD ? targetSysUiFlags : 0); onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv); // Passing the threshold from taskview to fullscreen app will vibrate final boolean passed = animator.getAnimatedFraction() >= SUCCESS_TRANSITION_PROGRESS; if (passed != passedOverviewThreshold[0]) { passedOverviewThreshold[0] = passed; performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } }); ClipAnimationHelper clipAnimationHelper = new ClipAnimationHelper(mActivity); clipAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this); clipAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */); AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, clipAnimationHelper); anim.play(progressAnim); anim.setDuration(duration); Consumer onTaskLaunchFinish = this::onTaskLaunched; mPendingAnimation = new PendingAnimation(anim); mPendingAnimation.addEndListener((onEndListener) -> { if (onEndListener.isSuccess) { Consumer onLaunchResult = (result) -> { onTaskLaunchFinish.accept(result); if (!result) { tv.notifyTaskLaunchFailed(TAG); } }; tv.launchTask(false, onLaunchResult, getHandler()); Task task = tv.getTask(); if (task != null) { mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( onEndListener.logAction, Direction.DOWN, indexOfChild(tv), TaskUtils.getLaunchComponentKeyForTask(task.key)); } } else { onTaskLaunchFinish.accept(false); } mPendingAnimation = null; }); return mPendingAnimation; } protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) { } public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); protected void onTaskLaunched(boolean success) { if (success) { resetTaskVisuals(); } } @Override protected void notifyPageSwitchListener(int prevPage) { super.notifyPageSwitchListener(prevPage); loadVisibleTaskData(); updateEnabledOverlays(); } @Override protected String getCurrentPageDescription() { return ""; } @Override public void addChildrenForAccessibility(ArrayList outChildren) { // Add children in reverse order for (int i = getChildCount() - 1; i >= 0; --i) { outChildren.add(getChildAt(i)); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); final AccessibilityNodeInfo.CollectionInfo collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( 1, getTaskViewCount(), false, AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); info.setCollectionInfo(collectionInfo); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); final int taskViewCount = getTaskViewCount(); event.setScrollable(taskViewCount > 0); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { final int[] visibleTasks = getVisibleChildrenRange(); event.setFromIndex(taskViewCount - visibleTasks[1] - 1); event.setToIndex(taskViewCount - visibleTasks[0] - 1); event.setItemCount(taskViewCount); } } @Override public CharSequence getAccessibilityClassName() { // To hear position-in-list related feedback from Talkback. return ListView.class.getName(); } @Override protected boolean isPageOrderFlipped() { return true; } public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) { mEnableDrawingLiveTile = enableDrawingLiveTile; } public void redrawLiveTile(boolean mightNeedToRefill) { } public void setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper) { mRecentsAnimationWrapper = recentsAnimationWrapper; } public void setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper) { mClipAnimationHelper = clipAnimationHelper; } public void setLiveTileOverlay(LiveTileOverlay liveTileOverlay) { mLiveTileOverlay = liveTileOverlay; } public void updateLiveTileIcon(Drawable icon) { if (mLiveTileOverlay != null) { mLiveTileOverlay.setIcon(icon); } } public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) { if (mRecentsAnimationWrapper == null) { if (onFinishComplete != null) { onFinishComplete.run(); } return; } mRecentsAnimationWrapper.finish(toRecents, onFinishComplete); } public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) { if (mDisallowScrollToClearAll != disallowScrollToClearAll) { mDisallowScrollToClearAll = disallowScrollToClearAll; updateMinAndMaxScrollX(); } } @Override protected int computeMinScrollX() { if (mIsRtl && mDisallowScrollToClearAll) { // We aren't showing the clear all button, so use the leftmost task as the min scroll. return getScrollForPage(getTaskViewCount() - 1); } return super.computeMinScrollX(); } @Override protected int computeMaxScrollX() { if (!mIsRtl && mDisallowScrollToClearAll) { // We aren't showing the clear all button, so use the rightmost task as the max scroll. return getScrollForPage(getTaskViewCount() - 1); } return super.computeMaxScrollX(); } public ClearAllButton getClearAllButton() { return mClearAllButton; } /** * @return How many pixels the running task is offset on the x-axis due to the current scrollX. */ public float getScrollOffset() { if (getRunningTaskIndex() == -1) { return 0; } int startScroll = getScrollForPage(getRunningTaskIndex()); int offsetX = startScroll - getScrollX(); offsetX *= getScaleX(); return offsetX; } public Consumer getEventDispatcher(RotationMode rotationMode) { if (rotationMode.isTransposed) { Matrix transform = new Matrix(); transform.setRotate(-rotationMode.surfaceRotation); if (getWidth() > 0 && getHeight() > 0) { float scale = ((float) getWidth()) / getHeight(); transform.postScale(scale, 1 / scale); } Matrix inverse = new Matrix(); transform.invert(inverse); return e -> { e.transform(transform); super.onTouchEvent(e); e.transform(inverse); }; } else { return super::onTouchEvent; } } public ClipAnimationHelper getTempClipAnimationHelper() { return mTempClipAnimationHelper; } private void updateEnabledOverlays() { int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1; int taskCount = getTaskViewCount(); for (int i = 0; i < taskCount; i++) { ((TaskView) getChildAt(i)).setOverlayEnabled(i == overlayEnabledPage); } } public void setOverlayEnabled(boolean overlayEnabled) { if (mOverlayEnabled != overlayEnabled) { mOverlayEnabled = overlayEnabled; updateEnabledOverlays(); } } public int getLeftGestureMargin() { final WindowInsets insets = getRootWindowInsets(); return Math.max(insets.getSystemGestureInsets().left, insets.getSystemWindowInsetLeft()); } public int getRightGestureMargin() { final WindowInsets insets = getRootWindowInsets(); return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight()); } }