summaryrefslogtreecommitdiffstats
path: root/quickstep/recents_ui_overrides/src/com
diff options
context:
space:
mode:
authorKevin <kevhan@google.com>2019-02-25 14:41:54 -0800
committerKevin <kevhan@google.com>2019-02-25 14:53:04 -0800
commit576d203a913680d8d81a3a39c1a19b8207a40985 (patch)
tree548a4c83fd9c0ebc1a921e6a3ac8684779ff21d4 /quickstep/recents_ui_overrides/src/com
parent6873fe517b1861e5695a0081152a10d08317b346 (diff)
downloadandroid_packages_apps_Trebuchet-576d203a913680d8d81a3a39c1a19b8207a40985.tar.gz
android_packages_apps_Trebuchet-576d203a913680d8d81a3a39c1a19b8207a40985.tar.bz2
android_packages_apps_Trebuchet-576d203a913680d8d81a3a39c1a19b8207a40985.zip
Remove several files from Recents Go src
As Go will not be using RecentsView, there are several files that are unneeded in the recents Go source. This moves these files to recents_ui_overrides where they will be used for non-Go builds. Bug: 114136250 Test: Build NexusLauncher, aospWithQuickstep, l3GoWithQuickstep Change-Id: I9d0154a75f4f2c71b865d414448e262b3019b2b9
Diffstat (limited to 'quickstep/recents_ui_overrides/src/com')
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/TaskViewTouchController.java309
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java132
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewTouchConsumer.java146
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepProcessInitializer.java54
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java162
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java115
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java77
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java333
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/TouchConsumer.java38
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionLog.java88
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java387
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java89
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java103
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewDrawable.java148
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java79
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java230
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java133
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java62
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java1627
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java294
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java382
-rw-r--r--quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java613
22 files changed, 5601 insertions, 0 deletions
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
new file mode 100644
index 000000000..fb1828b07
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2018 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.launcher3.uioverrides;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.FlingBlockCheck;
+import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.OverviewInteractionState;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+
+/**
+ * Touch controller for handling task view card swipes
+ */
+public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
+ extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener {
+
+ private static final String TAG = "OverviewSwipeController";
+
+ // Progress after which the transition is assumed to be a success in case user does not fling
+ public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+
+ protected final T mActivity;
+ private final SwipeDetector mDetector;
+ private final RecentsView mRecentsView;
+ private final int[] mTempCords = new int[2];
+
+ private PendingAnimation mPendingAnimation;
+ private AnimatorPlaybackController mCurrentAnimation;
+ private boolean mCurrentAnimationIsGoingUp;
+
+ private boolean mNoIntercept;
+
+ private float mDisplacementShift;
+ private float mProgressMultiplier;
+ private float mEndDisplacement;
+ private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
+
+ private TaskView mTaskBeingDragged;
+
+ public TaskViewTouchController(T activity) {
+ mActivity = activity;
+ mRecentsView = activity.getOverviewPanel();
+ mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL);
+ }
+
+ private boolean canInterceptTouch() {
+ if (mCurrentAnimation != null) {
+ // If we are already animating from a previous state, we can intercept.
+ return true;
+ }
+ if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
+ return false;
+ }
+ return isRecentsInteractive();
+ }
+
+ protected abstract boolean isRecentsInteractive();
+
+ protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
+ clearState();
+ }
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = !canInterceptTouch();
+ if (mNoIntercept) {
+ return false;
+ }
+
+ // Now figure out which direction scroll events the controller will start
+ // calling the callbacks.
+ int directionsToDetectScroll = 0;
+ boolean ignoreSlopWhenSettling = false;
+ if (mCurrentAnimation != null) {
+ directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+ ignoreSlopWhenSettling = true;
+ } else {
+ mTaskBeingDragged = null;
+
+ for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
+ TaskView view = mRecentsView.getTaskViewAt(i);
+ if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
+ .isEventOverView(view, ev)) {
+ mTaskBeingDragged = view;
+ if (!OverviewInteractionState.INSTANCE.get(mActivity)
+ .isSwipeUpGestureEnabled()) {
+ // Don't allow swipe down to open if we don't support swipe up
+ // to enter overview.
+ directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+ } else {
+ // The task can be dragged up to dismiss it,
+ // and down to open if it's the current page.
+ directionsToDetectScroll = i == mRecentsView.getCurrentPage()
+ ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE;
+ }
+ break;
+ }
+ }
+ if (mTaskBeingDragged == null) {
+ mNoIntercept = true;
+ return false;
+ }
+ }
+
+ mDetector.setDetectableScrollConditions(
+ directionsToDetectScroll, ignoreSlopWhenSettling);
+ }
+
+ if (mNoIntercept) {
+ return false;
+ }
+
+ onControllerTouchEvent(ev);
+ return mDetector.isDraggingOrSettling();
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ return mDetector.onTouchEvent(ev);
+ }
+
+ private void reInitAnimationController(boolean goingUp) {
+ if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
+ // No need to init
+ return;
+ }
+ int scrollDirections = mDetector.getScrollDirections();
+ if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0)
+ || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) {
+ // Trying to re-init in an unsupported direction.
+ return;
+ }
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.setPlayFraction(0);
+ }
+ if (mPendingAnimation != null) {
+ mPendingAnimation.finish(false, Touch.SWIPE);
+ mPendingAnimation = null;
+ }
+
+ mCurrentAnimationIsGoingUp = goingUp;
+ BaseDragLayer dl = mActivity.getDragLayer();
+ long maxDuration = (long) (2 * dl.getHeight());
+
+ if (goingUp) {
+ mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
+ true /* animateTaskView */, true /* removeTask */, maxDuration);
+
+ mEndDisplacement = -mTaskBeingDragged.getHeight();
+ } else {
+ mPendingAnimation = mRecentsView.createTaskLauncherAnimation(
+ mTaskBeingDragged, maxDuration);
+ mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
+
+ mTempCords[1] = mTaskBeingDragged.getHeight();
+ dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
+ mEndDisplacement = dl.getHeight() - mTempCords[1];
+ }
+
+ if (mCurrentAnimation != null) {
+ mCurrentAnimation.setOnCancelRunnable(null);
+ }
+ mCurrentAnimation = AnimatorPlaybackController
+ .wrap(mPendingAnimation.anim, maxDuration, this::clearState);
+ onUserControlledAnimationCreated(mCurrentAnimation);
+ mCurrentAnimation.getTarget().addListener(this);
+ mCurrentAnimation.dispatchOnStart();
+ mProgressMultiplier = 1 / mEndDisplacement;
+ }
+
+ @Override
+ public void onDragStart(boolean start) {
+ if (mCurrentAnimation == null) {
+ reInitAnimationController(mDetector.wasInitialTouchPositive());
+ mDisplacementShift = 0;
+ } else {
+ mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
+ mCurrentAnimation.pause();
+ }
+ mFlingBlockCheck.unblockFling();
+ }
+
+ @Override
+ public boolean onDrag(float displacement) {
+ float totalDisplacement = displacement + mDisplacementShift;
+ boolean isGoingUp =
+ totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
+ if (isGoingUp != mCurrentAnimationIsGoingUp) {
+ reInitAnimationController(isGoingUp);
+ mFlingBlockCheck.blockFling();
+ } else {
+ mFlingBlockCheck.onEvent();
+ }
+ mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
+
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
+ mRecentsView.redrawLiveTile(true);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onDragEnd(float velocity, boolean fling) {
+ final boolean goingToEnd;
+ final int logAction;
+ boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
+ if (blockedFling) {
+ fling = false;
+ }
+ float progress = mCurrentAnimation.getProgressFraction();
+ float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
+ if (fling) {
+ logAction = Touch.FLING;
+ boolean goingUp = velocity < 0;
+ goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
+ } else {
+ logAction = Touch.SWIPE;
+ goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
+ }
+ long animationDuration = SwipeDetector.calculateDuration(
+ velocity, goingToEnd ? (1 - progress) : progress);
+ if (blockedFling && !goingToEnd) {
+ animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
+ }
+
+ float nextFrameProgress = Utilities.boundToRange(
+ progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);
+
+ mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
+
+ ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+ anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
+ anim.setDuration(animationDuration);
+ anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ anim.addUpdateListener(valueAnimator -> {
+ if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
+ mRecentsView.redrawLiveTile(true);
+ }
+ });
+ }
+ if (QUICKSTEP_SPRINGS.get()) {
+ mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
+ }
+ anim.start();
+ }
+
+ private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
+ if (mPendingAnimation != null) {
+ mPendingAnimation.finish(wasSuccess, logAction);
+ mPendingAnimation = null;
+ }
+ clearState();
+ }
+
+ private void clearState() {
+ mDetector.finishedScrolling();
+ mDetector.setDetectableScrollConditions(0, false);
+ mTaskBeingDragged = null;
+ mCurrentAnimation = null;
+ if (mPendingAnimation != null) {
+ mPendingAnimation.finish(false, Touch.SWIPE);
+ mPendingAnimation = null;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 000000000..9fceab4ac
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,132 @@
+/*
+ * 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;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.StringJoiner;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+ private static final String TAG = "MultiStateCallback";
+ public static final boolean DEBUG_STATES = false;
+
+ private final SparseArray<Runnable> mCallbacks = new SparseArray<>();
+ private final SparseArray<Consumer<Boolean>> mStateChangeHandlers = new SparseArray<>();
+
+ private final String[] mStateNames;
+
+ public MultiStateCallback(String[] stateNames) {
+ mStateNames = DEBUG_STATES ? stateNames : null;
+ }
+
+ private int mState = 0;
+
+ /**
+ * Adds the provided state flags to the global state and executes any callbacks as a result.
+ */
+ public void setState(int stateFlag) {
+ if (DEBUG_STATES) {
+ Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
+ + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
+ }
+
+ int oldState = mState;
+ mState = mState | stateFlag;
+
+ int count = mCallbacks.size();
+ for (int i = 0; i < count; i++) {
+ int state = mCallbacks.keyAt(i);
+
+ if ((mState & state) == state) {
+ Runnable callback = mCallbacks.valueAt(i);
+ if (callback != null) {
+ // Set the callback to null, so that it does not run again.
+ mCallbacks.setValueAt(i, null);
+ callback.run();
+ }
+ }
+ }
+ notifyStateChangeHandlers(oldState);
+ }
+
+ /**
+ * Adds the provided state flags to the global state and executes any change handlers
+ * as a result.
+ */
+ public void clearState(int stateFlag) {
+ if (DEBUG_STATES) {
+ Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
+ + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
+ }
+
+ int oldState = mState;
+ mState = mState & ~stateFlag;
+ notifyStateChangeHandlers(oldState);
+ }
+
+ private void notifyStateChangeHandlers(int oldState) {
+ int count = mStateChangeHandlers.size();
+ for (int i = 0; i < count; i++) {
+ int state = mStateChangeHandlers.keyAt(i);
+ boolean wasOn = (state & oldState) == state;
+ boolean isOn = (state & mState) == state;
+
+ if (wasOn != isOn) {
+ mStateChangeHandlers.valueAt(i).accept(isOn);
+ }
+ }
+ }
+
+ /**
+ * Sets the callbacks to be run when the provided states are enabled.
+ * The callback is only run once.
+ */
+ public void addCallback(int stateMask, Runnable callback) {
+ mCallbacks.put(stateMask, callback);
+ }
+
+ /**
+ * Sets the handler to be called when the provided states are enabled or disabled.
+ */
+ public void addChangeHandler(int stateMask, Consumer<Boolean> handler) {
+ mStateChangeHandlers.put(stateMask, handler);
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ public boolean hasStates(int stateMask) {
+ return (mState & stateMask) == stateMask;
+ }
+
+ private String convertToFlagNames(int flags) {
+ StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
+ for (int i = 0; i < mStateNames.length; i++) {
+ if ((flags & (1 << i)) != 0) {
+ joiner.add(mStateNames[i]);
+ }
+ }
+ return joiner.toString();
+ }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewTouchConsumer.java
new file mode 100644
index 000000000..4da52e15c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewTouchConsumer.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
+
+import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.util.CachedEventDispatcher;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/**
+ * Touch consumer for handling touch on the recents/Launcher activity.
+ */
+public class OverviewTouchConsumer<T extends BaseDraggingActivity>
+ implements TouchConsumer {
+
+ private final CachedEventDispatcher mCachedEventDispatcher = new CachedEventDispatcher();
+ private final T mActivity;
+ private final BaseDragLayer mTarget;
+ private final int[] mLocationOnScreen = new int[2];
+ private final PointF mDownPos = new PointF();
+ private final int mTouchSlop;
+
+ private final boolean mStartingInActivityBounds;
+
+ private boolean mTrackingStarted = false;
+ private boolean mInvalidated = false;
+
+ OverviewTouchConsumer(T activity, boolean startingInActivityBounds) {
+ mActivity = activity;
+ mTarget = activity.getDragLayer();
+ mTouchSlop = ViewConfiguration.get(mActivity).getScaledTouchSlop();
+ mStartingInActivityBounds = startingInActivityBounds;
+ }
+
+ @Override
+ public void accept(MotionEvent ev) {
+ if (mInvalidated) {
+ return;
+ }
+ mCachedEventDispatcher.dispatchEvent(ev);
+ int action = ev.getActionMasked();
+ if (action == ACTION_DOWN) {
+ if (mStartingInActivityBounds) {
+ startTouchTracking(ev, false /* updateLocationOffset */,
+ false /* closeActiveWindows */);
+ return;
+ }
+ mTrackingStarted = false;
+ mDownPos.set(ev.getX(), ev.getY());
+ } else if (!mTrackingStarted) {
+ switch (action) {
+ case ACTION_CANCEL:
+ case ACTION_UP:
+ startTouchTracking(ev, true /* updateLocationOffset */,
+ false /* closeActiveWindows */);
+ break;
+ case ACTION_MOVE: {
+ float displacement = mActivity.getDeviceProfile().isLandscape ?
+ ev.getX() - mDownPos.x : ev.getY() - mDownPos.y;
+ if (Math.abs(displacement) >= mTouchSlop) {
+ // Start tracking only when mTouchSlop is crossed.
+ startTouchTracking(ev, true /* updateLocationOffset */,
+ true /* closeActiveWindows */);
+ }
+ }
+ }
+ }
+
+ if (action == ACTION_UP || action == ACTION_CANCEL) {
+ mInvalidated = true;
+
+ // Set an empty consumer to that all the cached events are cleared
+ if (!mCachedEventDispatcher.hasConsumer()) {
+ mCachedEventDispatcher.setConsumer(NO_OP);
+ }
+ }
+ }
+
+ private void startTouchTracking(MotionEvent ev, boolean updateLocationOffset,
+ boolean closeActiveWindows) {
+ if (updateLocationOffset) {
+ mTarget.getLocationOnScreen(mLocationOnScreen);
+ }
+
+ if (closeActiveWindows) {
+ OverviewCallbacks.get(mActivity).closeAllWindows();
+ ActivityManagerWrapper.getInstance()
+ .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
+ TOUCH_INTERACTION_LOG.startQuickStep();
+ }
+
+ mTrackingStarted = true;
+ mCachedEventDispatcher.setConsumer(this::sendEvent);
+
+ }
+
+ private void sendEvent(MotionEvent ev) {
+ if (mInvalidated || !mTarget.verifyTouchDispatch(this, ev)) {
+ mInvalidated = true;
+ return;
+ }
+ int flags = ev.getEdgeFlags();
+ ev.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR);
+ ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
+ if (ev.getAction() == ACTION_DOWN) {
+ mTarget.onInterceptTouchEvent(ev);
+ }
+ mTarget.onTouchEvent(ev);
+ ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
+ ev.setEdgeFlags(flags);
+ }
+
+ public static TouchConsumer newInstance(ActivityControlHelper activityHelper,
+ boolean startingInActivityBounds) {
+ BaseDraggingActivity activity = activityHelper.getCreatedActivity();
+ if (activity == null) {
+ return TouchConsumer.NO_OP;
+ }
+ return new OverviewTouchConsumer(activity, startingInActivityBounds);
+ }
+} \ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepProcessInitializer.java
new file mode 100644
index 000000000..befeee0db
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.launcher3.BuildConfig;
+import com.android.launcher3.MainProcessInitializer;
+import com.android.systemui.shared.system.ThreadedRendererCompat;
+
+@SuppressWarnings("unused")
+public class QuickstepProcessInitializer extends MainProcessInitializer {
+
+ private static final String TAG = "QuickstepProcessInitializer";
+
+ public QuickstepProcessInitializer(Context context) { }
+
+ @Override
+ protected void init(Context context) {
+ // Workaround for b/120550382, an external app can cause the launcher process to start for
+ // a work profile user which we do not support. Disable the application immediately when we
+ // detect this to be the case.
+ UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ if (um.isManagedProfile()) {
+ PackageManager pm = context.getPackageManager();
+ pm.setApplicationEnabledSetting(context.getPackageName(),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0 /* flags */);
+ Log.w(TAG, "Disabling " + BuildConfig.APPLICATION_ID
+ + ", unable to run in a managed profile");
+ return;
+ }
+
+ super.init(context);
+
+ // Elevate GPU priority for Quickstep and Remote animations.
+ ThreadedRendererCompat.setContextPriority(ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
new file mode 100644
index 000000000..5e7c1a1c5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper around RecentsAnimationController to help with some synchronization
+ */
+public class RecentsAnimationWrapper {
+
+ // A list of callbacks to run when we receive the recents animation target. There are different
+ // than the state callbacks as these run on the current worker thread.
+ private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
+
+ public SwipeAnimationTargetSet targetSet;
+
+ private boolean mWindowThresholdCrossed = false;
+
+ private final InputConsumerController mInputConsumer;
+ private final Supplier<TouchConsumer> mTouchProxySupplier;
+
+ private TouchConsumer mTouchConsumer;
+ private boolean mTouchInProgress;
+
+ private boolean mFinishPending;
+
+ public RecentsAnimationWrapper(InputConsumerController inputConsumer,
+ Supplier<TouchConsumer> touchProxySupplier) {
+ mInputConsumer = inputConsumer;
+ mTouchProxySupplier = touchProxySupplier;
+ }
+
+ @UiThread
+ public synchronized void setController(SwipeAnimationTargetSet targetSet) {
+ Preconditions.assertUIThread();
+ this.targetSet = targetSet;
+
+ if (targetSet == null) {
+ return;
+ }
+ targetSet.setWindowThresholdCrossed(mWindowThresholdCrossed);
+
+ if (!mCallbacks.isEmpty()) {
+ for (Runnable action : new ArrayList<>(mCallbacks)) {
+ action.run();
+ }
+ mCallbacks.clear();
+ }
+ }
+
+ public synchronized void runOnInit(Runnable action) {
+ if (targetSet == null) {
+ mCallbacks.add(action);
+ } else {
+ action.run();
+ }
+ }
+
+ /**
+ * @param onFinishComplete A callback that runs on the main thread after the animation
+ * controller has finished on the background thread.
+ */
+ @UiThread
+ public void finish(boolean toRecents, Runnable onFinishComplete) {
+ Preconditions.assertUIThread();
+ if (!toRecents) {
+ finishAndClear(false, onFinishComplete);
+ } else {
+ if (mTouchInProgress) {
+ mFinishPending = true;
+ // Execute the callback
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ } else {
+ finishAndClear(true, onFinishComplete);
+ }
+ }
+ }
+
+ private void finishAndClear(boolean toRecents, Runnable onFinishComplete) {
+ SwipeAnimationTargetSet controller = targetSet;
+ targetSet = null;
+ if (controller != null) {
+ controller.finishController(toRecents, onFinishComplete);
+ }
+ }
+
+ public void enableInputConsumer() {
+ if (targetSet != null) {
+ targetSet.enableInputConsumer();
+ }
+ }
+
+ /**
+ * Indicates that the gesture has crossed the window boundary threshold and system UI can be
+ * update the represent the window behind
+ */
+ public void setWindowThresholdCrossed(boolean windowThresholdCrossed) {
+ if (mWindowThresholdCrossed != windowThresholdCrossed) {
+ mWindowThresholdCrossed = windowThresholdCrossed;
+ if (targetSet != null) {
+ targetSet.setWindowThresholdCrossed(windowThresholdCrossed);
+ }
+ }
+ }
+
+ public void enableTouchProxy() {
+ mInputConsumer.setTouchListener(this::onInputConsumerTouch);
+ }
+
+ private boolean onInputConsumerTouch(MotionEvent ev) {
+ int action = ev.getAction();
+ if (action == ACTION_DOWN) {
+ mTouchInProgress = true;
+ mTouchConsumer = mTouchProxySupplier.get();
+ } else if (action == ACTION_CANCEL || action == ACTION_UP) {
+ // Finish any pending actions
+ mTouchInProgress = false;
+ if (mFinishPending) {
+ mFinishPending = false;
+ finishAndClear(true /* toRecents */, null);
+ }
+ }
+ if (mTouchConsumer != null) {
+ mTouchConsumer.accept(ev);
+ }
+
+ return true;
+ }
+
+ public SwipeAnimationTargetSet getController() {
+ return targetSet;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
new file mode 100644
index 000000000..7c6638a44
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.util.Log;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+
+/**
+ * Utility class used to store state information shared across multiple transitions.
+ */
+public class SwipeSharedState implements SwipeAnimationListener {
+
+ private final OverviewComponentObserver mOverviewComponentObserver;
+
+ private RecentsAnimationListenerSet mRecentsAnimationListener;
+ private SwipeAnimationTargetSet mLastAnimationTarget;
+
+ private boolean mLastAnimationCancelled = false;
+ private boolean mLastAnimationRunning = false;
+
+ public boolean canGestureBeContinued;
+ public boolean goingToLauncher;
+
+ public SwipeSharedState(OverviewComponentObserver overviewComponentObserver) {
+ mOverviewComponentObserver = overviewComponentObserver;
+ }
+
+ @Override
+ public final void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+ mLastAnimationTarget = targetSet;
+
+ mLastAnimationCancelled = false;
+ mLastAnimationRunning = true;
+ }
+
+ @Override
+ public final void onRecentsAnimationCanceled() {
+ mLastAnimationTarget = null;
+
+ mLastAnimationCancelled = true;
+ mLastAnimationRunning = false;
+ }
+
+ private void clearListenerState() {
+ if (mRecentsAnimationListener != null) {
+ mRecentsAnimationListener.removeListener(this);
+ }
+ mRecentsAnimationListener = null;
+ mLastAnimationTarget = null;
+ mLastAnimationCancelled = false;
+ mLastAnimationRunning = false;
+ }
+
+ private void onSwipeAnimationFinished(SwipeAnimationTargetSet targetSet) {
+ if (mLastAnimationTarget == targetSet) {
+ mLastAnimationRunning = false;
+ }
+ }
+
+ public RecentsAnimationListenerSet newRecentsAnimationListenerSet() {
+ Preconditions.assertUIThread();
+
+ if (mLastAnimationRunning) {
+ String msg = "New animation started before completing old animation";
+ if (FeatureFlags.IS_DOGFOOD_BUILD) {
+ throw new IllegalArgumentException(msg);
+ } else {
+ Log.e("SwipeSharedState", msg, new Exception());
+ }
+ }
+
+ clearListenerState();
+ mRecentsAnimationListener = new RecentsAnimationListenerSet(mOverviewComponentObserver
+ .getActivityControlHelper().shouldMinimizeSplitScreen(),
+ this::onSwipeAnimationFinished);
+ mRecentsAnimationListener.addListener(this);
+ return mRecentsAnimationListener;
+ }
+
+ public RecentsAnimationListenerSet getActiveListener() {
+ return mRecentsAnimationListener;
+ }
+
+ public void applyActiveRecentsAnimationState(SwipeAnimationListener listener) {
+ if (mLastAnimationTarget != null) {
+ listener.onRecentsAnimationStart(mLastAnimationTarget);
+ } else if (mLastAnimationCancelled) {
+ listener.onRecentsAnimationCanceled();
+ }
+ }
+
+ public void clearAllState() {
+ clearListenerState();
+ canGestureBeContinued = false;
+ goingToLauncher = false;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
new file mode 100644
index 000000000..d979c991a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.graphics.Matrix;
+import android.view.View;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Factory class to create and add an overlays on the TaskView
+ */
+public class TaskOverlayFactory implements ResourceBasedOverride {
+
+ /** Note that these will be shown in order from top to bottom, if available for the task. */
+ private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
+ new TaskSystemShortcut.AppInfo(),
+ new TaskSystemShortcut.SplitScreen(),
+ new TaskSystemShortcut.Pin(),
+ new TaskSystemShortcut.Install(),
+ new TaskSystemShortcut.Freeform()
+ };
+
+ public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
+ new MainThreadInitializedObject<>(c -> Overrides.getObject(TaskOverlayFactory.class,
+ c, R.string.task_overlay_factory_class));
+
+ public TaskOverlay createOverlay(View thumbnailView) {
+ return new TaskOverlay();
+ }
+
+ public static class TaskOverlay {
+
+ public void setTaskInfo(Task task, ThumbnailData thumbnail, Matrix matrix) {
+ }
+
+ public void reset() {
+ }
+
+ public List<TaskSystemShortcut> getEnabledShortcuts(TaskView taskView) {
+ final ArrayList<TaskSystemShortcut> shortcuts = new ArrayList<>();
+ final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
+ for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
+ View.OnClickListener onClickListener =
+ menuOption.getOnClickListener(activity, taskView);
+ if (onClickListener != null) {
+ shortcuts.add(menuOption);
+ }
+ }
+ return shortcuts;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
new file mode 100644
index 000000000..42a28fbfa
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.View;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityCompat;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
+
+ private static final String TAG = "TaskSystemShortcut";
+
+ protected T mSystemShortcut;
+
+ public TaskSystemShortcut(T systemShortcut) {
+ super(systemShortcut);
+ mSystemShortcut = systemShortcut;
+ }
+
+ protected TaskSystemShortcut(int iconResId, int labelResId) {
+ super(iconResId, labelResId);
+ }
+
+ @Override
+ public View.OnClickListener getOnClickListener(
+ BaseDraggingActivity activity, ItemInfo itemInfo) {
+ return null;
+ }
+
+ public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
+ Task task = view.getTask();
+
+ ShortcutInfo dummyInfo = new ShortcutInfo();
+ dummyInfo.intent = new Intent();
+ ComponentName component = task.getTopComponent();
+ dummyInfo.intent.setComponent(component);
+ dummyInfo.user = UserHandle.of(task.key.userId);
+ dummyInfo.title = TaskUtils.getTitle(activity, task);
+
+ return getOnClickListenerForTask(activity, task, dummyInfo);
+ }
+
+ protected View.OnClickListener getOnClickListenerForTask(
+ BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) {
+ return mSystemShortcut.getOnClickListener(activity, dummyInfo);
+ }
+
+ public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
+ public AppInfo() {
+ super(new SystemShortcut.AppInfo());
+ }
+ }
+
+ public static abstract class MultiWindow extends TaskSystemShortcut {
+
+ private Handler mHandler;
+
+ public MultiWindow(int iconRes, int textRes) {
+ super(iconRes, textRes);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ protected abstract boolean isAvailable(BaseDraggingActivity activity);
+ protected abstract ActivityOptions makeLaunchOptions(Activity activity);
+ protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
+
+ @Override
+ public View.OnClickListener getOnClickListener(
+ BaseDraggingActivity activity, TaskView taskView) {
+ if (!isAvailable(activity)) {
+ return null;
+ }
+ final Task task = taskView.getTask();
+ final int taskId = task.key.id;
+ if (!task.isDockable) {
+ return null;
+ }
+ final RecentsView recentsView = activity.getOverviewPanel();
+
+ final TaskThumbnailView thumbnailView = taskView.getThumbnail();
+ return (v -> {
+ final View.OnLayoutChangeListener onLayoutChangeListener =
+ new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int l, int t, int r, int b,
+ int oldL, int oldT, int oldR, int oldB) {
+ taskView.getRootView().removeOnLayoutChangeListener(this);
+ recentsView.clearIgnoreResetTask(taskId);
+
+ // Start animating in the side pages once launcher has been resized
+ recentsView.dismissTask(taskView, false, false);
+ }
+ };
+
+ final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
+ new DeviceProfile.OnDeviceProfileChangeListener() {
+ @Override
+ public void onDeviceProfileChanged(DeviceProfile dp) {
+ activity.removeOnDeviceProfileChangeListener(this);
+ if (dp.isMultiWindowMode) {
+ taskView.getRootView().addOnLayoutChangeListener(
+ onLayoutChangeListener);
+ }
+ }
+ };
+
+ dismissTaskMenuView(activity);
+
+ ActivityOptions options = makeLaunchOptions(activity);
+ if (options != null
+ && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+ options)) {
+ if (!onActivityStarted(activity)) {
+ return;
+ }
+ // Add a device profile change listener to kick off animating the side tasks
+ // once we enter multiwindow mode and relayout
+ activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
+
+ final Runnable animStartedListener = () -> {
+ // Hide the task view and wait for the window to be resized
+ // TODO: Consider animating in launcher and do an in-place start activity
+ // afterwards
+ recentsView.setIgnoreResetTask(taskId);
+ taskView.setAlpha(0f);
+ };
+
+ final int[] position = new int[2];
+ thumbnailView.getLocationOnScreen(position);
+ final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX());
+ final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY());
+ final Rect taskBounds = new Rect(position[0], position[1],
+ position[0] + width, position[1] + height);
+
+ // Take the thumbnail of the task without a scrim and apply it back after
+ float alpha = thumbnailView.getDimAlpha();
+ thumbnailView.setDimAlpha(0);
+ Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+ taskBounds.width(), taskBounds.height(), thumbnailView, 1f,
+ Color.BLACK);
+ thumbnailView.setDimAlpha(alpha);
+
+ AppTransitionAnimationSpecsFuture future =
+ new AppTransitionAnimationSpecsFuture(mHandler) {
+ @Override
+ public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+ return Collections.singletonList(new AppTransitionAnimationSpecCompat(
+ taskId, thumbnail, taskBounds));
+ }
+ };
+ WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
+ future, animStartedListener, mHandler, true /* scaleUp */,
+ v.getDisplay().getDisplayId());
+ }
+ });
+ }
+ }
+
+ public static class SplitScreen extends MultiWindow {
+ public SplitScreen() {
+ super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
+ }
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity) {
+ // Don't show menu-item if already in multi-window
+ return !activity.getDeviceProfile().isMultiWindowMode;
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ final ActivityCompat act = new ActivityCompat(activity);
+ final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
+ act.getDisplayId());
+ if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
+ return null;
+ }
+ boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
+ return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
+ try {
+ sysUiProxy.onSplitScreenInvoked();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to notify SysUI of split screen: ", e);
+ return false;
+ }
+ activity.getUserEventDispatcher().logActionOnControl(TAP,
+ LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
+ return true;
+ }
+ }
+
+ public static class Freeform extends MultiWindow {
+ public Freeform() {
+ super(R.drawable.ic_split_screen, R.string.recent_task_option_freeform);
+ }
+
+ @Override
+ protected boolean isAvailable(BaseDraggingActivity activity) {
+ return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
+ }
+
+ @Override
+ protected ActivityOptions makeLaunchOptions(Activity activity) {
+ return ActivityOptionsCompat.makeFreeformOptions();
+ }
+
+ @Override
+ protected boolean onActivityStarted(BaseDraggingActivity activity) {
+ Launcher.getLauncher(activity).getStateManager().goToState(LauncherState.NORMAL);
+ return true;
+ }
+ }
+
+ public static class Pin extends TaskSystemShortcut {
+
+ private static final String TAG = Pin.class.getSimpleName();
+
+ private Handler mHandler;
+
+ public Pin() {
+ super(R.drawable.ic_pin, R.string.recent_task_option_pin);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public View.OnClickListener getOnClickListener(
+ BaseDraggingActivity activity, TaskView taskView) {
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
+ if (sysUiProxy == null) {
+ return null;
+ }
+ if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
+ return null;
+ }
+ if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
+ // We shouldn't be able to pin while an app is locked.
+ return null;
+ }
+ return view -> {
+ Consumer<Boolean> resultCallback = success -> {
+ if (success) {
+ try {
+ sysUiProxy.startScreenPinning(taskView.getTask().key.id);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to start screen pinning: ", e);
+ }
+ } else {
+ taskView.notifyTaskLaunchFailed(TAG);
+ }
+ };
+ taskView.launchTask(true, resultCallback, mHandler);
+ dismissTaskMenuView(activity);
+ };
+ }
+ }
+
+ public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
+ public Install() {
+ super(new SystemShortcut.Install());
+ }
+
+ @Override
+ protected View.OnClickListener getOnClickListenerForTask(
+ BaseDraggingActivity activity, Task task, ItemInfo itemInfo) {
+ if (InstantAppResolver.newInstance(activity).isInstantApp(activity,
+ task.getTopComponent().getPackageName())) {
+ return mSystemShortcut.createOnClickListener(activity, itemInfo);
+ }
+ return null;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchConsumer.java
new file mode 100644
index 000000000..12ae7b633
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchConsumer.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.MotionEvent;
+
+import java.util.function.Consumer;
+
+@TargetApi(Build.VERSION_CODES.O)
+@FunctionalInterface
+public interface TouchConsumer extends Consumer<MotionEvent> {
+
+ TouchConsumer NO_OP = (ev) -> {};
+
+ default boolean isActive() {
+ return false;
+ }
+
+ /**
+ * Called by the event queue when the consumer is about to be switched to a new consumer.
+ */
+ default void onConsumerAboutToBeSwitched() { }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionLog.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionLog.java
new file mode 100644
index 000000000..b54270192
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionLog.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import android.view.MotionEvent;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.LinkedList;
+
+/**
+ * Keeps track of debugging logs for a particular quickstep gesture.
+ */
+public class TouchInteractionLog {
+
+ // The number of gestures to log
+ private static final int MAX_NUM_LOG_GESTURES = 5;
+
+ private final Calendar mCalendar = Calendar.getInstance();
+ private final SimpleDateFormat mDateFormat = new SimpleDateFormat("MMM dd - kk:mm:ss:SSS");
+ private final LinkedList<ArrayList<String>> mGestureLogs = new LinkedList<>();
+
+ public void prepareForNewGesture() {
+ mGestureLogs.add(new ArrayList<>());
+ while (mGestureLogs.size() > MAX_NUM_LOG_GESTURES) {
+ mGestureLogs.pop();
+ }
+ getCurrentLog().add("[" + mDateFormat.format(mCalendar.getTime()) + "]");
+ }
+
+ public void setTouchConsumer(TouchConsumer consumer) {
+ getCurrentLog().add("tc=" + consumer.getClass().getSimpleName());
+ }
+
+ public void addMotionEvent(MotionEvent event) {
+ getCurrentLog().add("ev=" + event.getActionMasked());
+ }
+
+ public void startQuickStep() {
+ getCurrentLog().add("qstStart");
+ }
+
+ public void startRecentsAnimation() {
+ getCurrentLog().add("raStart");
+ }
+
+ public void startRecentsAnimationCallback(int numTargets) {
+ getCurrentLog().add("raStartCb=" + numTargets);
+ }
+
+ public void cancelRecentsAnimation() {
+ getCurrentLog().add("raCancel");
+ }
+
+ public void finishRecentsAnimation(boolean toHome) {
+ getCurrentLog().add("raFinish=" + toHome);
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println("TouchInteractionLog {");
+ for (ArrayList<String> gesture : mGestureLogs) {
+ pw.print(" ");
+ for (String log : gesture) {
+ pw.print(log + " ");
+ }
+ pw.println();
+ }
+ pw.println("}");
+ }
+
+ private ArrayList<String> getCurrentLog() {
+ return mGestureLogs.getLast();
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
new file mode 100644
index 000000000..6d374c61b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.RemoteException;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.recents.utilities.RectFEvaluator;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+import com.android.systemui.shared.system.TransactionCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.function.BiFunction;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Utility class to handle window clip animation
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class ClipAnimationHelper {
+
+ // The bounds of the source app in device coordinates
+ private final Rect mSourceStackBounds = new Rect();
+ // The insets of the source app
+ private final Rect mSourceInsets = new Rect();
+ // The source app bounds with the source insets applied, in the source app window coordinates
+ private final RectF mSourceRect = new RectF();
+ // The bounds of the task view in launcher window coordinates
+ private final RectF mTargetRect = new RectF();
+ // The insets to be used for clipping the app window, which can be larger than mSourceInsets
+ // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
+ // app window coordinates.
+ private final RectF mSourceWindowClipInsets = new RectF();
+ // The insets to be used for clipping the app window. For live tile, we don't transform the clip
+ // relative to the target rect.
+ private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
+
+ // The bounds of launcher (not including insets) in device coordinates
+ public final Rect mHomeStackBounds = new Rect();
+
+ // The clip rect in source app window coordinates
+ private final RectF mClipRectF = new RectF();
+ private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
+ private final Matrix mTmpMatrix = new Matrix();
+ private final RectF mTmpRectF = new RectF();
+ private final RectF mCurrentRectWithInsets = new RectF();
+ // Corner radius of windows, in pixels
+ private final float mWindowCornerRadius;
+ // Corner radius of windows when they're in overview mode.
+ private final float mTaskCornerRadius;
+ // If windows can have real time rounded corners.
+ private final boolean mSupportsRoundedCornersOnWindows;
+
+ // Corner radius currently applied to transformed window.
+ private float mCurrentCornerRadius;
+
+ // Whether to boost the opening animation target layers, or the closing
+ private int mBoostModeTargetLayers = -1;
+
+ private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback =
+ (t, a1) -> a1;
+
+ public ClipAnimationHelper(Context context) {
+ mWindowCornerRadius = RecentsModel.INSTANCE.get(context).getWindowCornerRadius();
+ mSupportsRoundedCornersOnWindows = RecentsModel.INSTANCE.get(context)
+ .supportsRoundedCornersOnWindows();
+ int taskCornerRadiusRes = mSupportsRoundedCornersOnWindows ?
+ R.dimen.task_corner_radius : R.dimen.task_corner_radius_small;
+ mTaskCornerRadius = context.getResources().getDimension(taskCornerRadiusRes);
+ }
+
+ private void updateSourceStack(RemoteAnimationTargetCompat target) {
+ mSourceInsets.set(target.contentInsets);
+ mSourceStackBounds.set(target.sourceContainerBounds);
+
+ // TODO: Should sourceContainerBounds already have this offset?
+ mSourceStackBounds.offsetTo(target.position.x, target.position.y);
+
+ }
+
+ public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
+ mHomeStackBounds.set(homeStackBounds);
+ updateSourceStack(target);
+ }
+
+ public void updateTargetRect(Rect targetRect) {
+ mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
+ mSourceStackBounds.width() - mSourceInsets.right,
+ mSourceStackBounds.height() - mSourceInsets.bottom);
+ mTargetRect.set(targetRect);
+ mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
+ mHomeStackBounds.top - mSourceStackBounds.top);
+
+ // Calculate the clip based on the target rect (since the content insets and the
+ // launcher insets may differ, so the aspect ratio of the target rect can differ
+ // from the source rect. The difference between the target rect (scaled to the
+ // source rect) is the amount to clip on each edge.
+ RectF scaledTargetRect = new RectF(mTargetRect);
+ Utilities.scaleRectFAboutCenter(scaledTargetRect,
+ mSourceRect.width() / mTargetRect.width());
+ scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
+ mSourceWindowClipInsets.set(
+ Math.max(scaledTargetRect.left, 0),
+ Math.max(scaledTargetRect.top, 0),
+ Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
+ Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
+ mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
+ mSourceRect.set(scaledTargetRect);
+ }
+
+ public void prepareAnimation(boolean isOpening) {
+ mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
+ }
+
+ public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
+ if (params.currentRect == null) {
+ RectF currentRect;
+ mTmpRectF.set(mTargetRect);
+ Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
+ float progress = params.progress;
+ currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+ currentRect.offset(params.offsetX, 0);
+
+ final RectF sourceWindowClipInsets = params.forLiveTile
+ ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
+ mClipRectF.left = sourceWindowClipInsets.left * progress;
+ mClipRectF.top = sourceWindowClipInsets.top * progress;
+ mClipRectF.right =
+ mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
+ mClipRectF.bottom =
+ mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
+ params.setCurrentRectAndTargetAlpha(currentRect, 1);
+ }
+
+ SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
+ for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
+ RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
+ mTmpMatrix.setTranslate(app.position.x, app.position.y);
+ Rect crop = app.sourceContainerBounds;
+ float alpha = 1f;
+ int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
+ float cornerRadius = 0f;
+ float scale = params.currentRect.width() / crop.width();
+ if (app.mode == targetSet.targetMode) {
+ if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+ mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
+ mTmpMatrix.postTranslate(app.position.x, app.position.y);
+ mClipRectF.roundOut(crop);
+ if (mSupportsRoundedCornersOnWindows) {
+ cornerRadius = Utilities.mapRange(params.progress, mWindowCornerRadius,
+ mTaskCornerRadius);
+ }
+ }
+ alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
+ } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ crop = null;
+ layer = Integer.MAX_VALUE;
+ }
+
+ // Since radius is in Surface space, but we draw the rounded corners in screen space, we
+ // have to undo the scale.
+ surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
+ cornerRadius / scale);
+ }
+ applySurfaceParams(params.syncTransactionApplier, surfaceParams);
+ return params.currentRect;
+ }
+
+ public RectF getCurrentRectWithInsets() {
+ mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
+ return mCurrentRectWithInsets;
+ }
+
+ private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat
+ syncTransactionApplier, SurfaceParams[] params) {
+ if (syncTransactionApplier != null) {
+ syncTransactionApplier.scheduleApply(params);
+ } else {
+ TransactionCompat t = new TransactionCompat();
+ for (SurfaceParams param : params) {
+ SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
+ }
+ t.setEarlyWakeup();
+ t.apply();
+ }
+ }
+
+ public void setTaskAlphaCallback(
+ BiFunction<RemoteAnimationTargetCompat, Float, Float> callback) {
+ mTaskAlphaCallback = callback;
+ }
+
+ public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
+ fromTaskThumbnailView(ttv, rv, null);
+ }
+
+ public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
+ @Nullable RemoteAnimationTargetCompat target) {
+ BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
+ BaseDragLayer dl = activity.getDragLayer();
+
+ int[] pos = new int[2];
+ dl.getLocationOnScreen(pos);
+ mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
+ mHomeStackBounds.offset(pos[0], pos[1]);
+
+ if (target != null) {
+ updateSourceStack(target);
+ } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
+ updateStackBoundsToMultiWindowTaskSize(activity);
+ } else {
+ mSourceStackBounds.set(mHomeStackBounds);
+ Rect fallback = dl.getInsets();
+ mSourceInsets.set(ttv.getInsets(fallback));
+ }
+
+ Rect targetRect = new Rect();
+ dl.getDescendantRectRelativeToSelf(ttv, targetRect);
+ updateTargetRect(targetRect);
+
+ if (target == null) {
+ // Transform the clip relative to the target rect. Only do this in the case where we
+ // aren't applying the insets to the app windows (where the clip should be in target app
+ // space)
+ float scale = mTargetRect.width() / mSourceRect.width();
+ mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
+ mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
+ mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
+ mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
+ }
+ }
+
+ private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
+ ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
+ if (sysUiProxy != null) {
+ try {
+ mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
+ return;
+ } catch (RemoteException e) {
+ // Use half screen size
+ }
+ }
+
+ // Assume that the task size is half screen size (minus the insets and the divider size)
+ DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
+ // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
+ // account for system insets
+ int taskWidth = fullDp.availableWidthPx;
+ int taskHeight = fullDp.availableHeightPx;
+ int halfDividerSize = activity.getResources()
+ .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
+
+ Rect insets = new Rect();
+ WindowManagerWrapper.getInstance().getStableInsets(insets);
+ if (fullDp.isLandscape) {
+ taskWidth = taskWidth / 2 - halfDividerSize;
+ } else {
+ taskHeight = taskHeight / 2 - halfDividerSize;
+ }
+
+ // Align the task to bottom left/right edge (closer to nav bar).
+ int left = activity.getDeviceProfile().isSeascape() ? insets.left
+ : (insets.left + fullDp.availableWidthPx - taskWidth);
+ mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
+ mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
+ }
+
+ public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) {
+ RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
+ canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left,
+ mSourceStackBounds.top - mHomeStackBounds.top);
+ mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL);
+
+ canvas.concat(mTmpMatrix);
+ canvas.translate(mTargetRect.left, mTargetRect.top);
+
+ float scale = mTargetRect.width() / mSourceRect.width();
+ float insetProgress = (1 - progress);
+ ttv.drawOnCanvas(canvas,
+ -mSourceWindowClipInsets.left * insetProgress,
+ -mSourceWindowClipInsets.top * insetProgress,
+ ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress,
+ ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress,
+ Utilities.mapRange(progress, mWindowCornerRadius * scale, ttv.getCornerRadius()));
+ }
+
+ public RectF getTargetRect() {
+ return mTargetRect;
+ }
+
+ public RectF getSourceRect() {
+ return mSourceRect;
+ }
+
+ public float getCurrentCornerRadius() {
+ return mCurrentCornerRadius;
+ }
+
+ public static class TransformParams {
+ float progress;
+ public float offsetX;
+ public float offsetScale;
+ @Nullable RectF currentRect;
+ float targetAlpha;
+ boolean forLiveTile;
+
+ SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
+
+ public TransformParams() {
+ progress = 0;
+ offsetX = 0;
+ offsetScale = 1;
+ currentRect = null;
+ targetAlpha = 0;
+ forLiveTile = false;
+ }
+
+ public TransformParams setProgress(float progress) {
+ this.progress = progress;
+ this.currentRect = null;
+ return this;
+ }
+
+ public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
+ this.currentRect = currentRect;
+ this.targetAlpha = targetAlpha;
+ return this;
+ }
+
+ public TransformParams setOffsetX(float offsetX) {
+ this.offsetX = offsetX;
+ return this;
+ }
+
+ public TransformParams setOffsetScale(float offsetScale) {
+ this.offsetScale = offsetScale;
+ return this;
+ }
+
+ public TransformParams setForLiveTile(boolean forLiveTile) {
+ this.forLiveTile = forLiveTile;
+ return this;
+ }
+
+ public TransformParams setSyncTransactionApplier(
+ SyncRtSurfaceTransactionApplierCompat applier) {
+ this.syncTransactionApplier = applier;
+ return this;
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
new file mode 100644
index 000000000..62f2183b5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RecentsAnimationListenerSet.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+
+import android.graphics.Rect;
+import android.util.ArraySet;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+import androidx.annotation.UiThread;
+
+/**
+ * Wrapper around {@link RecentsAnimationListener} which delegates callbacks to multiple listeners
+ * on the main thread
+ */
+public class RecentsAnimationListenerSet implements RecentsAnimationListener {
+
+ private final Set<SwipeAnimationListener> mListeners = new ArraySet<>();
+ private final boolean mShouldMinimizeSplitScreen;
+ private final Consumer<SwipeAnimationTargetSet> mOnFinishListener;
+
+ public RecentsAnimationListenerSet(boolean shouldMinimizeSplitScreen,
+ Consumer<SwipeAnimationTargetSet> onFinishListener) {
+ mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+ mOnFinishListener = onFinishListener;
+ }
+
+ @UiThread
+ public void addListener(SwipeAnimationListener listener) {
+ Preconditions.assertUIThread();
+ mListeners.add(listener);
+ }
+
+ @UiThread
+ public void removeListener(SwipeAnimationListener listener) {
+ Preconditions.assertUIThread();
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public final void onAnimationStart(RecentsAnimationControllerCompat controller,
+ RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
+ Rect minimizedHomeBounds) {
+ SwipeAnimationTargetSet targetSet = new SwipeAnimationTargetSet(controller, targets,
+ homeContentInsets, minimizedHomeBounds, mShouldMinimizeSplitScreen,
+ mOnFinishListener);
+ Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+ for (SwipeAnimationListener listener : getListeners()) {
+ listener.onRecentsAnimationStart(targetSet);
+ }
+ });
+ }
+
+ @Override
+ public final void onAnimationCanceled() {
+ Utilities.postAsyncCallback(MAIN_THREAD_EXECUTOR.getHandler(), () -> {
+ for (SwipeAnimationListener listener : getListeners()) {
+ listener.onRecentsAnimationCanceled();
+ }
+ });
+ }
+
+ private SwipeAnimationListener[] getListeners() {
+ return mListeners.toArray(new SwipeAnimationListener[mListeners.size()]);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
new file mode 100644
index 000000000..b6824813f
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/SwipeAnimationTargetSet.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+
+import android.graphics.Rect;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+
+import java.util.function.Consumer;
+
+/**
+ * Extension of {@link RemoteAnimationTargetSet} with additional information about swipe
+ * up animation
+ */
+public class SwipeAnimationTargetSet extends RemoteAnimationTargetSet {
+
+ private final boolean mShouldMinimizeSplitScreen;
+ private final Consumer<SwipeAnimationTargetSet> mOnFinishListener;
+
+
+ public final RecentsAnimationControllerCompat controller;
+ public final Rect homeContentInsets;
+ public final Rect minimizedHomeBounds;
+
+ public SwipeAnimationTargetSet(RecentsAnimationControllerCompat controller,
+ RemoteAnimationTargetCompat[] targets, Rect homeContentInsets,
+ Rect minimizedHomeBounds, boolean shouldMinimizeSplitScreen,
+ Consumer<SwipeAnimationTargetSet> onFinishListener) {
+ super(targets, MODE_CLOSING);
+ this.controller = controller;
+ this.homeContentInsets = homeContentInsets;
+ this.minimizedHomeBounds = minimizedHomeBounds;
+ this.mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+ this.mOnFinishListener = onFinishListener;
+ }
+
+ public void finishController(boolean toRecents, Runnable callback) {
+ mOnFinishListener.accept(this);
+ BACKGROUND_EXECUTOR.execute(() -> {
+ controller.setInputConsumerEnabled(false);
+ controller.finish(toRecents);
+
+ if (callback != null) {
+ MAIN_THREAD_EXECUTOR.execute(callback);
+ }
+ });
+ }
+
+ public void enableInputConsumer() {
+ BACKGROUND_EXECUTOR.submit(() -> {
+ controller.hideCurrentInputMethod();
+ controller.setInputConsumerEnabled(true);
+ });
+ }
+
+ public void setWindowThresholdCrossed(boolean thresholdCrossed) {
+ BACKGROUND_EXECUTOR.execute(() -> {
+ controller.setAnimationTargetsBehindSystemBars(!thresholdCrossed);
+ if (mShouldMinimizeSplitScreen && thresholdCrossed) {
+ // NOTE: As a workaround for conflicting animations (Launcher animating the task
+ // leash, and SystemUI resizing the docked stack, which resizes the task), we
+ // currently only set the minimized mode, and not the inverse.
+ // TODO: Synchronize the minimize animation with the launcher animation
+ controller.setSplitScreenMinimized(thresholdCrossed);
+ }
+ });
+ }
+
+ public ThumbnailData screenshotTask(int taskId) {
+ return controller != null ? controller.screenshotTask(taskId) : null;
+ }
+
+ public interface SwipeAnimationListener {
+
+ void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet);
+
+ void onRecentsAnimationCanceled();
+ }
+
+ public interface SwipeAnimationFinishListener {
+
+ void onSwipeAnimationFinished(SwipeAnimationTargetSet targetSet);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewDrawable.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewDrawable.java
new file mode 100644
index 000000000..10283bfc5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewDrawable.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.Utilities;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+
+public class TaskViewDrawable extends Drawable {
+
+ public static final FloatProperty<TaskViewDrawable> PROGRESS =
+ new FloatProperty<TaskViewDrawable>("progress") {
+ @Override
+ public void setValue(TaskViewDrawable taskViewDrawable, float v) {
+ taskViewDrawable.setProgress(v);
+ }
+
+ @Override
+ public Float get(TaskViewDrawable taskViewDrawable) {
+ return taskViewDrawable.mProgress;
+ }
+ };
+
+ /**
+ * The progress at which we play the atomic icon scale animation.
+ */
+ private static final float ICON_SCALE_THRESHOLD = 0.95f;
+
+ private final RecentsView mParent;
+ private final View mIconView;
+ private final int[] mIconPos;
+ private final TaskView mTaskView;
+
+ private final TaskThumbnailView mThumbnailView;
+
+ private final ClipAnimationHelper mClipAnimationHelper;
+
+ private float mProgress = 1;
+ private boolean mPassedIconScaleThreshold;
+ private ValueAnimator mIconScaleAnimator;
+ private float mIconScale;
+
+ public TaskViewDrawable(TaskView tv, RecentsView parent) {
+ mParent = parent;
+ mTaskView = tv;
+ mIconView = tv.getIconView();
+ mIconPos = new int[2];
+ mIconScale = mIconView.getScaleX();
+ Utilities.getDescendantCoordRelativeToAncestor(mIconView, parent, mIconPos, true);
+
+ mThumbnailView = tv.getThumbnail();
+ mClipAnimationHelper = new ClipAnimationHelper(parent.getContext());
+ mClipAnimationHelper.fromTaskThumbnailView(mThumbnailView, parent);
+ }
+
+ public void setProgress(float progress) {
+ mProgress = progress;
+ mParent.invalidate();
+ boolean passedIconScaleThreshold = progress <= ICON_SCALE_THRESHOLD;
+ if (mPassedIconScaleThreshold != passedIconScaleThreshold) {
+ mPassedIconScaleThreshold = passedIconScaleThreshold;
+ animateIconScale(mPassedIconScaleThreshold ? 0 : 1);
+ }
+ }
+
+ private void animateIconScale(float toScale) {
+ if (mIconScaleAnimator != null) {
+ mIconScaleAnimator.cancel();
+ }
+ mIconScaleAnimator = ValueAnimator.ofFloat(mIconScale, toScale);
+ mIconScaleAnimator.addUpdateListener(valueAnimator -> {
+ mIconScale = (float) valueAnimator.getAnimatedValue();
+ if (mProgress > ICON_SCALE_THRESHOLD) {
+ // Speed up the icon scale to ensure it is 1 when progress is 1.
+ float iconProgress = (mProgress - ICON_SCALE_THRESHOLD) / (1 - ICON_SCALE_THRESHOLD);
+ if (iconProgress > mIconScale) {
+ mIconScale = iconProgress;
+ }
+ }
+ invalidateSelf();
+ });
+ mIconScaleAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIconScaleAnimator = null;
+ }
+ });
+ mIconScaleAnimator.setDuration(TaskView.SCALE_ICON_DURATION);
+ mIconScaleAnimator.start();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.save();
+ canvas.translate(mParent.getScrollX(), mParent.getScrollY());
+ mClipAnimationHelper.drawForProgress(mThumbnailView, canvas, mProgress);
+ canvas.restore();
+
+ canvas.save();
+ canvas.translate(mIconPos[0], mIconPos[1]);
+ canvas.scale(mIconScale, mIconScale, mIconView.getWidth() / 2, mIconView.getHeight() / 2);
+ mIconView.draw(canvas);
+ canvas.restore();
+ }
+
+ public ClipAnimationHelper getClipAnimationHelper() {
+ return mClipAnimationHelper;
+ }
+
+ @Override
+ public void setAlpha(int i) { }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) { }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ public TaskView getTaskView() {
+ return mTaskView;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
new file mode 100644
index 000000000..fbecd8486
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/ClearAllButton.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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 android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+import com.android.launcher3.Utilities;
+import com.android.quickstep.views.RecentsView.PageCallbacks;
+import com.android.quickstep.views.RecentsView.ScrollState;
+
+public class ClearAllButton extends Button implements PageCallbacks {
+
+ private float mScrollAlpha = 1;
+ private float mContentAlpha = 1;
+
+ private final boolean mIsRtl;
+
+ private int mScrollOffset;
+
+ public ClearAllButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mIsRtl = Utilities.isRtl(context.getResources());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ RecentsView parent = (RecentsView) getParent();
+ mScrollOffset = mIsRtl ? parent.getPaddingRight() / 2 : - parent.getPaddingLeft() / 2;
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void setContentAlpha(float alpha) {
+ if (mContentAlpha != alpha) {
+ mContentAlpha = alpha;
+ updateAlpha();
+ }
+ }
+
+ @Override
+ public void onPageScroll(ScrollState scrollState) {
+ float width = getWidth();
+ if (width == 0) {
+ return;
+ }
+
+ float shift = Math.min(scrollState.scrollFromEdge, width);
+ setTranslationX(mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift));
+ mScrollAlpha = 1 - shift / width;
+ updateAlpha();
+ }
+
+ private void updateAlpha() {
+ final float alpha = mScrollAlpha * mContentAlpha;
+ setAlpha(alpha);
+ setClickable(alpha == 1);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
new file mode 100644
index 000000000..5fe92d5b6
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2018 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 android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
+
+import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.res.Resources;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.StringRes;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.util.Locale;
+
+public final class DigitalWellBeingToast extends LinearLayout {
+ static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
+
+ public interface InitializeCallback {
+ void call(float saturation, String contentDescription);
+ }
+
+ private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
+
+ private Task mTask;
+ private ImageView mImage;
+ private TextView mText;
+
+ public DigitalWellBeingToast(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setLayoutDirection(Utilities.isRtl(getResources()) ?
+ View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
+ setOnClickListener((view) -> openAppUsageSettings());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mText = findViewById(R.id.digital_well_being_remaining_time);
+ mImage = findViewById(R.id.digital_well_being_hourglass);
+ }
+
+ public void initialize(Task task, InitializeCallback callback) {
+ mTask = task;
+
+ if (task.key.userId != UserHandle.myUserId()) {
+ setVisibility(GONE);
+ callback.call(1, task.titleDescription);
+ return;
+ }
+
+ Utilities.THREAD_POOL_EXECUTOR.execute(() -> {
+ long appUsageLimitTimeMs = -1;
+ long appRemainingTimeMs = -1;
+
+ try {
+ final Method getAppUsageLimit = LauncherApps.class.getMethod(
+ "getAppUsageLimit",
+ String.class,
+ UserHandle.class);
+ final Object usageLimit = getAppUsageLimit.invoke(
+ getContext().getSystemService(LauncherApps.class),
+ task.getTopComponent().getPackageName(),
+ UserHandle.of(task.key.userId));
+
+ if (usageLimit != null) {
+ final Class appUsageLimitClass = usageLimit.getClass();
+ appUsageLimitTimeMs = (long) appUsageLimitClass.getMethod("getTotalUsageLimit").
+ invoke(usageLimit);
+ appRemainingTimeMs = (long) appUsageLimitClass.getMethod("getUsageRemaining").
+ invoke(usageLimit);
+ }
+ } catch (Exception e) {
+ // Do nothing
+ }
+
+ final long appUsageLimitTimeMsFinal = appUsageLimitTimeMs;
+ final long appRemainingTimeMsFinal = appRemainingTimeMs;
+
+ post(() -> {
+ if (appUsageLimitTimeMsFinal < 0) {
+ setVisibility(GONE);
+ } else {
+ setVisibility(VISIBLE);
+ mText.setText(getText(appRemainingTimeMsFinal));
+ mImage.setImageResource(appRemainingTimeMsFinal > 0 ?
+ R.drawable.hourglass_top : R.drawable.hourglass_bottom);
+ }
+
+ callback.call(
+ appUsageLimitTimeMsFinal >= 0 && appRemainingTimeMsFinal <= 0 ? 0 : 1,
+ getContentDescriptionForTask(
+ task, appUsageLimitTimeMsFinal, appRemainingTimeMsFinal));
+ });
+ });
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId,
+ boolean forceFormatWidth) {
+ int hours = Math.toIntExact(duration.toHours());
+ int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
+
+ // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero.
+ if (hours > 0 && minutes > 0) {
+ return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute)
+ .formatMeasures(
+ new Measure(hours, MeasureUnit.HOUR),
+ new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced).
+ if (hours > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault(),
+ forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(hours, MeasureUnit.HOUR));
+ }
+
+ // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced).
+ if (minutes > 0) {
+ return MeasureFormat.getInstance(
+ Locale.getDefault()
+ , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE));
+ }
+
+ // Use a specific string for usage less than one minute but non-zero.
+ if (duration.compareTo(Duration.ZERO) > 0) {
+ return getResources().getString(durationLessThanOneMinuteStringId);
+ }
+
+ // Otherwise, return 0-minute string.
+ return MeasureFormat.getInstance(
+ Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE)
+ .formatMeasures(new Measure(0, MeasureUnit.MINUTE));
+ }
+
+ private String getReadableDuration(
+ Duration duration,
+ FormatWidth formatWidthHourAndMinute,
+ @StringRes int durationLessThanOneMinuteStringId) {
+ return getReadableDuration(
+ duration,
+ formatWidthHourAndMinute,
+ durationLessThanOneMinuteStringId,
+ /* forceFormatWidth= */ false);
+ }
+
+ private String getShorterReadableDuration(Duration duration) {
+ return getReadableDuration(
+ duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
+ }
+
+ private String getText(long remainingTime) {
+ final Resources resources = getResources();
+ return (remainingTime <= 0) ?
+ resources.getString(R.string.app_in_grayscale) :
+ resources.getString(
+ R.string.time_left_for_app,
+ getShorterReadableDuration(Duration.ofMillis(remainingTime)));
+ }
+
+ public void openAppUsageSettings() {
+ final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+ .putExtra(Intent.EXTRA_PACKAGE_NAME,
+ mTask.getTopComponent().getPackageName()).addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ try {
+ final Launcher launcher = Launcher.getLauncher(getContext());
+ final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
+ this, 0, 0,
+ getWidth(), getHeight());
+ launcher.startActivity(intent, options.toBundle());
+ launcher.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
+ LauncherLogProto.ControlType.APP_USAGE_SETTINGS, this);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Failed to open app usage settings for task "
+ + mTask.getTopComponent().getPackageName(), e);
+ }
+ }
+
+ private String getContentDescriptionForTask(
+ Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
+ return appUsageLimitTimeMs >= 0 ?
+ getResources().getString(
+ R.string.task_contents_description_with_remaining_time,
+ task.titleDescription,
+ getText(appRemainingTimeMs)) :
+ task.titleDescription;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
new file mode 100644
index 000000000..eb8da6e58
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/IconView.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018 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 android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.launcher3.FastBitmapDrawable;
+
+import java.util.ArrayList;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+public class IconView extends View {
+
+ public interface OnScaleUpdateListener {
+ public void onScaleUpdate(float scale);
+ }
+
+ private Drawable mDrawable;
+
+ private ArrayList<OnScaleUpdateListener> mScaleListeners;
+
+ public IconView(Context context) {
+ super(context);
+ }
+
+ public IconView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void setDrawable(Drawable d) {
+ if (mDrawable != null) {
+ mDrawable.setCallback(null);
+ }
+ mDrawable = d;
+ if (mDrawable != null) {
+ mDrawable.setCallback(this);
+ mDrawable.setBounds(0, 0, getWidth(), getHeight());
+ }
+ invalidate();
+ }
+
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (mDrawable != null) {
+ mDrawable.setBounds(0, 0, w, h);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mDrawable;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ final Drawable drawable = mDrawable;
+ if (drawable != null && drawable.isStateful()
+ && drawable.setState(getDrawableState())) {
+ invalidateDrawable(drawable);
+ }
+ }
+
+ @Override
+ public void invalidateDrawable(@NonNull Drawable drawable) {
+ super.invalidateDrawable(drawable);
+ if (drawable instanceof FastBitmapDrawable && mScaleListeners != null) {
+ for (OnScaleUpdateListener listener : mScaleListeners) {
+ listener.onScaleUpdate(((FastBitmapDrawable) drawable).getScale());
+ }
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mDrawable != null) {
+ mDrawable.draw(canvas);
+ }
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ return false;
+ }
+
+ public void addUpdateScaleListener(OnScaleUpdateListener listener) {
+ if (mScaleListeners == null) {
+ mScaleListeners = new ArrayList<>();
+ }
+ mScaleListeners.add(listener);
+ if (mDrawable instanceof FastBitmapDrawable) {
+ listener.onScaleUpdate(((FastBitmapDrawable) mDrawable).getScale());
+ }
+ }
+
+ public void removeUpdateScaleListener(OnScaleUpdateListener listener) {
+ if (mScaleListeners != null) {
+ mScaleListeners.remove(listener);
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
new file mode 100644
index 000000000..ab2b90ff1
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
@@ -0,0 +1,62 @@
+package com.android.quickstep.views;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+
+public class LiveTileOverlay extends Drawable {
+
+ private final Paint mPaint = new Paint();
+
+ private Rect mBoundsRect = new Rect();
+ private RectF mCurrentRect;
+ private float mCornerRadius;
+
+ private boolean mDrawEnabled = true;
+
+ public LiveTileOverlay() {
+ mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ }
+
+ public void update(RectF currentRect, float cornerRadius) {
+ invalidateSelf();
+
+ mCurrentRect = currentRect;
+ mCornerRadius = cornerRadius;
+
+ mCurrentRect.roundOut(mBoundsRect);
+ setBounds(mBoundsRect);
+ invalidateSelf();
+ }
+
+ public void setDrawEnabled(boolean drawEnabled) {
+ if (mDrawEnabled != drawEnabled) {
+ mDrawEnabled = drawEnabled;
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mCurrentRect != null && mDrawEnabled) {
+ canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
+ }
+ }
+
+ @Override
+ public void setAlpha(int i) { }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) { }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
new file mode 100644
index 000000000..8faf95d5b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -0,0 +1,1627 @@
+/*
+ * 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 com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+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.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.uioverrides.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
+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.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+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.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+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.LauncherAnimUtils.ViewProgressProperty;
+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.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+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.OverviewCallbacks;
+import com.android.quickstep.RecentsAnimationWrapper;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.quickstep.util.TaskViewDrawable;
+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.BackgroundExecutor;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.WindowCallbacksCompat;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * A list of recent tasks.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
+ TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
+ InvariantDeviceProfile.OnIDPChangeListener {
+
+ private static final String TAG = RecentsView.class.getSimpleName();
+
+ public static final float SPRING_MIN_VISIBLE_CHANGE = 0.001f;
+ public static final float SPRING_DAMPING_RATIO = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
+ public static final float SPRING_STIFFNESS = SpringForce.STIFFNESS_MEDIUM;
+
+ public static final FloatProperty<RecentsView> CONTENT_ALPHA =
+ new FloatProperty<RecentsView>("contentAlpha") {
+ @Override
+ public void setValue(RecentsView view, float v) {
+ view.setContentAlpha(v);
+ }
+
+ @Override
+ public Float get(RecentsView view) {
+ return view.getContentAlpha();
+ }
+ };
+
+ 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;
+
+ private static final float[] sTempFloatArray = new float[3];
+
+ 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();
+
+ 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<TaskView> mTaskViewPool;
+
+ /**
+ * TODO: Call reloadIdNeeded in onTaskStackChanged.
+ */
+ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+ @Override
+ public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
+ if (!mHandleTaskStackChanges) {
+ return;
+ }
+ updateThumbnail(taskId, snapshot);
+ }
+
+ @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;
+ }
+
+ BackgroundExecutor.get().submit(() -> {
+ 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
+ private int mRunningTaskId = -1;
+ private boolean mRunningTaskTileHidden;
+ private Task mTmpRunningTask;
+
+ private boolean mRunningTaskIconScaledDown = false;
+
+ private boolean mOverviewStateEnabled;
+ private boolean mHandleTaskStackChanges;
+ private Runnable mNextPageSwitchRunnable;
+ private boolean mSwipeDownShouldLaunchApp;
+ private boolean mTouchDownToStartHome;
+ private final int mTouchSlop;
+ private int mDownX;
+ private int mDownY;
+
+ private PendingAnimation mPendingAnimation;
+ private LayoutTransition mLayoutTransition;
+
+ @ViewDebug.ExportedProperty(category = "launcher")
+ private float mContentAlpha = 1;
+
+ // 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 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);
+
+ 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);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ 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));
+ mEmptyMessagePadding = getResources()
+ .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
+ setWillNotDraw(false);
+ updateEmptyMessage();
+ }
+
+ public OverScroller getScroller() {
+ return mScroller;
+ }
+
+ public boolean isRtl() {
+ return mIsRtl;
+ }
+
+ 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);
+ mIdp.addOnChangeListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ updateTaskStackListenerState();
+ mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
+ mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ mSyncTransactionApplier = null;
+ 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;
+ Task task = taskView.getTask();
+ if (mHasVisibleTaskData.get(task.key.id)) {
+ mHasVisibleTaskData.delete(task.key.id);
+ taskView.onTaskListVisibilityChanged(false /* visible */);
+ }
+ 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 setNextPageSwitchRunnable(Runnable r) {
+ mNextPageSwitchRunnable = r;
+ }
+
+ @Override
+ protected void onPageEndTransition() {
+ super.onPageEndTransition();
+ if (mNextPageSwitchRunnable != null) {
+ mNextPageSwitchRunnable.run();
+ mNextPageSwitchRunnable = null;
+ }
+ 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() || Math.hypot(mDownX - x, mDownY - y) > mTouchSlop)) {
+ 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 true;
+ }
+
+ private void applyLoadPlan(ArrayList<Task> 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();
+ }
+
+ 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();
+ }
+ }
+ if (mRunningTaskTileHidden) {
+ setRunningTaskHidden(mRunningTaskTileHidden);
+ }
+
+ // Force apply the scale.
+ if (mIgnoreResetTaskId != mRunningTaskId) {
+ applyRunningTaskIconScale();
+ }
+
+ updateCurveProperties();
+ // Update the set of visible task's data
+ loadVisibleTaskData();
+ }
+
+ 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() {
+ setRunningTaskViewShowScreenshot(false);
+ mRunningTaskId = -1;
+ mRunningTaskTileHidden = false;
+ mIgnoreResetTaskId = -1;
+ mTaskListChangeId = -1;
+
+ mRecentsAnimationWrapper = null;
+ mClipAnimationHelper = null;
+
+ unloadVisibleTaskData();
+ setCurrentPage(0);
+
+ OverviewCallbacks.get(getContext()).onResetOverview();
+ }
+
+ /**
+ * Reloads the view if anything in recents changed.
+ */
+ public void reloadIfNeeded() {
+ if (!mModel.isTaskListValid(mTaskListChangeId)) {
+ mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
+ }
+ }
+
+ /**
+ * Ensures that the first task in the view represents {@param task} and reloads the view
+ * if needed. This allows the swipe-up gesture to assume that the first tile always
+ * corresponds to the correct task.
+ * 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 showTask(int runningTaskId) {
+ if (getChildCount() == 0) {
+ // Add an empty view for now until the task plan is loaded and applied
+ final TaskView taskView = mTaskViewPool.getView();
+ addView(taskView);
+ 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);
+ }
+ setCurrentTask(runningTaskId);
+ }
+
+ public TaskView getRunningTaskView() {
+ return getTaskView(mRunningTaskId);
+ }
+
+ public int getRunningTaskIndex() {
+ TaskView tv = getRunningTaskView();
+ return tv == null ? -1 : indexOfChild(tv);
+ }
+
+ /**
+ * Hides the tile associated with {@link #mRunningTaskId}
+ */
+ public void setRunningTaskHidden(boolean isHidden) {
+ mRunningTaskTileHidden = isHidden;
+ TaskView runningTask = getRunningTaskView();
+ if (runningTask != null) {
+ runningTask.setAlpha(isHidden ? 0 : mContentAlpha);
+ }
+ }
+
+ /**
+ * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile.
+ */
+ public void setCurrentTask(int runningTaskId) {
+ boolean runningTaskTileHidden = mRunningTaskTileHidden;
+ boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown;
+
+ setRunningTaskIconScaledDown(false);
+ setRunningTaskHidden(false);
+ setRunningTaskViewShowScreenshot(true);
+ mRunningTaskId = runningTaskId;
+ setRunningTaskViewShowScreenshot(false);
+ setRunningTaskIconScaledDown(runningTaskIconScaledDown);
+ setRunningTaskHidden(runningTaskTileHidden);
+
+ setCurrentPage(getRunningTaskIndex());
+
+ // Load the tasks (if the loading is already
+ mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
+ }
+
+ 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() {
+ mRunningTaskIconScaledDown = false;
+ TaskView firstTask = getRunningTaskView();
+ if (firstTask != null) {
+ firstTask.animateIconScaleAndDimIntoView();
+ }
+ }
+
+ 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<>(new ViewProgressProperty(taskView,
+ View.TRANSLATION_Y), "taskViewTransY", SPRING_MIN_VISIBLE_CHANGE,
+ SPRING_DAMPING_RATIO, SPRING_STIFFNESS, 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) {
+ mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+ onEndListener.logAction, Direction.UP, index,
+ TaskUtils.getLaunchComponentKeyForTask(task.key));
+ }
+ }
+ }
+
+ 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<>(
+ new ViewProgressProperty(child, View.TRANSLATION_X),
+ "taskViewTransX", SPRING_MIN_VISIBLE_CHANGE, SPRING_DAMPING_RATIO,
+ SPRING_STIFFNESS, 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<PendingAnimation.OnEndListener>() {
+ @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));
+ }
+
+ 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) {
+ getChildAt(i).setAlpha(alpha);
+ }
+ }
+ mClearAllButton.setContentAlpha(mContentAlpha);
+
+ int alphaInt = Math.round(alpha * 255);
+ mEmptyMessagePaint.setAlpha(alphaInt);
+ mEmptyIcon.setAlpha(alphaInt);
+
+ setVisibility(alpha > 0 ? VISIBLE : GONE);
+ }
+
+ private float[] getAdjacentScaleAndTranslation(TaskView currTask,
+ float currTaskToScale, float currTaskToTranslationY) {
+ float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale());
+ sTempFloatArray[0] = currTaskToScale;
+ sTempFloatArray[1] = mIsRtl ? -displacement : displacement;
+ sTempFloatArray[2] = currTaskToTranslationY;
+ return sTempFloatArray;
+ }
+
+ @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;
+
+ float toScale = clipAnimationHelper.getSourceRect().width()
+ / clipAnimationHelper.getTargetRect().width();
+ float toTranslationY = clipAnimationHelper.getSourceRect().centerY()
+ - clipAnimationHelper.getTargetRect().centerY();
+ if (launchingCenterTask) {
+ TaskView centerTask = getTaskViewAt(centerTaskIndex);
+ if (taskIndex - 1 >= 0) {
+ TaskView adjacentTask = getTaskViewAt(taskIndex - 1);
+ float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
+ toScale, toTranslationY);
+ scaleAndTranslation[1] = -scaleAndTranslation[1];
+ anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
+ anim.play(ObjectAnimator.ofFloat(adjacentTask, TaskView.FULLSCREEN_PROGRESS, 1));
+ }
+ if (taskIndex + 1 < getTaskViewCount()) {
+ TaskView adjacentTask = getTaskViewAt(taskIndex + 1);
+ float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask,
+ toScale, toTranslationY);
+ anim.play(createAnimForChild(adjacentTask, scaleAndTranslation));
+ anim.play(ObjectAnimator.ofFloat(adjacentTask, TaskView.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;
+ }
+
+ private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) {
+ AnimatorSet anim = new AnimatorSet();
+ anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0]));
+ anim.play(new PropertyListBuilder()
+ .translationX(toScaleAndTranslation[1])
+ .translationY(toScaleAndTranslation[2])
+ .build(child));
+ 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());
+ }
+
+ tv.setVisibility(INVISIBLE);
+ int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
+ TaskViewDrawable drawable = new TaskViewDrawable(tv, this);
+ getOverlay().add(drawable);
+
+ final boolean[] passedOverviewThreshold = new boolean[] {false};
+ ObjectAnimator drawableAnim =
+ ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0);
+ drawableAnim.setInterpolator(LINEAR);
+ drawableAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ TransformParams mParams = new TransformParams();
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator 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);
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mRecentsAnimationWrapper.targetSet != null
+ && drawable.getTaskView().isRunningTask()) {
+ mParams.setProgress(1 - animator.getAnimatedFraction())
+ .setSyncTransactionApplier(mSyncTransactionApplier)
+ .setForLiveTile(true);
+ drawable.getClipAnimationHelper().applyTransform(
+ mRecentsAnimationWrapper.targetSet, mParams);
+ } else {
+ redrawLiveTile(true);
+ }
+ }
+
+ // 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);
+ }
+ }
+ });
+
+ AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv,
+ drawable.getClipAnimationHelper());
+ anim.play(drawableAnim);
+ anim.setDuration(duration);
+
+ Consumer<Boolean> onTaskLaunchFinish = (result) -> {
+ onTaskLaunched(result);
+ tv.setVisibility(VISIBLE);
+ getOverlay().remove(drawable);
+ };
+
+ mPendingAnimation = new PendingAnimation(anim);
+ mPendingAnimation.addEndListener((onEndListener) -> {
+ if (onEndListener.isSuccess) {
+ Consumer<Boolean> 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;
+ }
+
+ public abstract boolean shouldUseMultiWindowTaskSizeStrategy();
+
+ protected void onTaskLaunched(boolean success) {
+ resetTaskVisuals();
+ }
+
+ @Override
+ protected void notifyPageSwitchListener(int prevPage) {
+ super.notifyPageSwitchListener(prevPage);
+ loadVisibleTaskData();
+ }
+
+ @Override
+ protected String getCurrentPageDescription() {
+ return "";
+ }
+
+ @Override
+ public void addChildrenForAccessibility(ArrayList<View> 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 finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
+ if (mRecentsAnimationWrapper == null) {
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ return;
+ }
+
+ mRecentsAnimationWrapper.finish(toRecents, onFinishComplete);
+ }
+
+ public void takeScreenshotAndFinishRecentsAnimation(boolean toRecents,
+ Runnable onFinishComplete) {
+ if (mRecentsAnimationWrapper == null || getRunningTaskView() == null) {
+ if (onFinishComplete != null) {
+ onFinishComplete.run();
+ }
+ return;
+ }
+
+ SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController();
+ if (controller != null) {
+ // Update the screenshot of the task
+ ThumbnailData taskSnapshot = controller.screenshotTask(mRunningTaskId);
+ TaskView taskView = updateThumbnail(mRunningTaskId, taskSnapshot);
+ if (taskView != null) {
+ taskView.setShowScreenshot(true);
+ // Defer finishing the animation until the next launcher frame with the
+ // new thumbnail
+ new WindowCallbacksCompat(taskView) {
+
+ // The number of frames to defer until we actually finish the animation
+ private int mDeferFrameCount = 2;
+
+ @Override
+ public void onPostDraw(Canvas canvas) {
+ if (mDeferFrameCount > 0) {
+ mDeferFrameCount--;
+ // Workaround, detach and reattach to invalidate the root node for
+ // another draw
+ detach();
+ attach();
+ taskView.invalidate();
+ return;
+ }
+
+ detach();
+ mRecentsAnimationWrapper.finish(toRecents, () -> {
+ onFinishComplete.run();
+ mRunningTaskId = -1;
+ });
+ }
+ }.attach();
+ }
+ }
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
new file mode 100644
index 000000000..682152e9a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2018 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 com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.TaskSystemShortcut;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.views.IconView.OnScaleUpdateListener;
+
+import java.util.List;
+
+/**
+ * Contains options for a recent task when long-pressing its icon.
+ */
+public class TaskMenuView extends AbstractFloatingView {
+
+ private static final Rect sTempRect = new Rect();
+
+ private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
+ @Override
+ public void onScaleUpdate(float scale) {
+ final Drawable drawable = mTaskIcon.getDrawable();
+ if (drawable instanceof FastBitmapDrawable) {
+ if (scale != ((FastBitmapDrawable) drawable).getScale()) {
+ mMenuIconDrawable.setScale(scale);
+ }
+ }
+ }
+ };
+
+ private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
+ @Override
+ public void onScaleUpdate(float scale) {
+ final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
+ if (taskViewDrawable instanceof FastBitmapDrawable) {
+ final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
+ if (currentScale != scale) {
+ ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
+ }
+ }
+ }
+ };
+
+ private static final int REVEAL_OPEN_DURATION = 150;
+ private static final int REVEAL_CLOSE_DURATION = 100;
+
+ private final float mThumbnailTopMargin;
+ private BaseDraggingActivity mActivity;
+ private TextView mTaskName;
+ private IconView mTaskIcon;
+ private AnimatorSet mOpenCloseAnimator;
+ private TaskView mTaskView;
+ private LinearLayout mOptionLayout;
+ private FastBitmapDrawable mMenuIconDrawable;
+
+ public TaskMenuView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ mActivity = BaseDraggingActivity.fromContext(context);
+ mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTaskName = findViewById(R.id.task_name);
+ mTaskIcon = findViewById(R.id.task_icon);
+ mOptionLayout = findViewById(R.id.menu_option_layout);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ BaseDragLayer dl = mActivity.getDragLayer();
+ if (!dl.isEventOverView(this, ev)) {
+ // TODO: log this once we have a new container type for it?
+ close(true);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void handleClose(boolean animate) {
+ if (animate) {
+ animateClose();
+ } else {
+ closeComplete();
+ }
+ }
+
+ @Override
+ public void logActionCommand(int command) {
+ // TODO
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ // Remove all scale listeners when menu is removed
+ mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
+ mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
+ }
+
+ @Override
+ protected boolean isOfType(int type) {
+ return (type & TYPE_TASK_MENU) != 0;
+ }
+
+ public void setPosition(float x, float y) {
+ setX(x);
+ setY(y + mThumbnailTopMargin);
+ }
+
+ public static TaskMenuView showForTask(TaskView taskView) {
+ BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext());
+ final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate(
+ R.layout.task_menu, activity.getDragLayer(), false);
+ return taskMenuView.populateAndShowForTask(taskView) ? taskMenuView : null;
+ }
+
+ private boolean populateAndShowForTask(TaskView taskView) {
+ if (isAttachedToWindow()) {
+ return false;
+ }
+ mActivity.getDragLayer().addView(this);
+ mTaskView = taskView;
+ addMenuOptions(mTaskView);
+ orientAroundTaskView(mTaskView);
+ post(this::animateOpen);
+ return true;
+ }
+
+ private void addMenuOptions(TaskView taskView) {
+ Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
+ mTaskIcon.setDrawable(icon);
+ mTaskIcon.setOnClickListener(v -> close(true));
+ mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
+ mTaskName.setOnClickListener(v -> close(true));
+
+ // Set the icons to match scale by listening to each other's changes
+ mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
+ taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
+ mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
+
+ // Move the icon and text up half an icon size to lay over the TaskView
+ LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
+ params.topMargin = (int) -mThumbnailTopMargin;
+ mTaskIcon.setLayoutParams(params);
+
+ final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext());
+ final List<TaskSystemShortcut> shortcuts =
+ taskView.getTaskOverlay().getEnabledShortcuts(taskView);
+ final int count = shortcuts.size();
+ for (int i = 0; i < count; ++i) {
+ final TaskSystemShortcut menuOption = shortcuts.get(i);
+ addMenuOption(menuOption, menuOption.getOnClickListener(activity, taskView));
+ }
+ }
+
+ private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) {
+ ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate(
+ R.layout.task_view_menu_option, this, false);
+ menuOption.setIconAndLabelFor(
+ menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ menuOptionView.setOnClickListener(
+ view -> mTaskView.getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
+ () -> onClickListener.onClick(view)));
+ } else {
+ menuOptionView.setOnClickListener(onClickListener);
+ }
+ mOptionLayout.addView(menuOptionView);
+ }
+
+ private void orientAroundTaskView(TaskView taskView) {
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
+ Rect insets = mActivity.getDragLayer().getInsets();
+ BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
+ params.width = taskView.getMeasuredWidth();
+ params.gravity = Gravity.START;
+ setLayoutParams(params);
+ setScaleX(taskView.getScaleX());
+ setScaleY(taskView.getScaleY());
+ setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top);
+ }
+
+ private void animateOpen() {
+ animateOpenOrClosed(false);
+ mIsOpen = true;
+ }
+
+ private void animateClose() {
+ animateOpenOrClosed(true);
+ }
+
+ private void animateOpenOrClosed(boolean closing) {
+ if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+ mOpenCloseAnimator.end();
+ }
+ mOpenCloseAnimator = new AnimatorSet();
+
+ final Animator revealAnimator = createOpenCloseOutlineProvider()
+ .createRevealAnimator(this, closing);
+ revealAnimator.setInterpolator(Interpolators.DEACCEL);
+ mOpenCloseAnimator.play(revealAnimator);
+ mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA,
+ closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA));
+ mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setVisibility(VISIBLE);
+ }
+
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ if (closing) {
+ closeComplete();
+ }
+ }
+ });
+ mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
+ mOpenCloseAnimator.start();
+ }
+
+ private void closeComplete() {
+ mIsOpen = false;
+ mActivity.getDragLayer().removeView(this);
+ }
+
+ private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
+ float radius = getResources().getDimension(R.dimen.task_corner_radius);
+ Rect fromRect = new Rect(0, 0, getWidth(), 0);
+ Rect toRect = new Rect(0, 0, getWidth(), getHeight());
+ return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
+ }
+
+ public View findMenuItemByText(String text) {
+ for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) {
+ final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i);
+ if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) {
+ return menuOptionView;
+ }
+ }
+ return null;
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
new file mode 100644
index 000000000..90604efef
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -0,0 +1,382 @@
+/*
+ * 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 com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Property;
+import android.view.View;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.util.Themes;
+import com.android.quickstep.TaskOverlayFactory;
+import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * A task in the Recents view.
+ */
+public class TaskThumbnailView extends View {
+
+ private final static ColorMatrix COLOR_MATRIX = new ColorMatrix();
+ private final static ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
+
+ public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
+ new FloatProperty<TaskThumbnailView>("dimAlpha") {
+ @Override
+ public void setValue(TaskThumbnailView thumbnail, float dimAlpha) {
+ thumbnail.setDimAlpha(dimAlpha);
+ }
+
+ @Override
+ public Float get(TaskThumbnailView thumbnailView) {
+ return thumbnailView.mDimAlpha;
+ }
+ };
+
+ private final float mCornerRadius;
+
+ private final BaseActivity mActivity;
+ private final TaskOverlay mOverlay;
+ private final boolean mIsDarkTextTheme;
+ private final Paint mPaint = new Paint();
+ private final Paint mBackgroundPaint = new Paint();
+ private final Paint mClearPaint = new Paint();
+ private final Paint mDimmingPaintAfterClearing = new Paint();
+
+ private final Matrix mMatrix = new Matrix();
+
+ private float mClipBottom = -1;
+ private Rect mScaledInsets = new Rect();
+ private boolean mIsRotated;
+
+ private Task mTask;
+ private ThumbnailData mThumbnailData;
+ protected BitmapShader mBitmapShader;
+
+ private float mDimAlpha = 1f;
+ private float mDimAlphaMultiplier = 1f;
+ private float mSaturation = 1f;
+
+ public TaskThumbnailView(Context context) {
+ this(context, null);
+ }
+
+ public TaskThumbnailView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius);
+ mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this);
+ mPaint.setFilterBitmap(true);
+ mBackgroundPaint.setColor(Color.WHITE);
+ mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ mDimmingPaintAfterClearing.setColor(Color.BLACK);
+ mActivity = BaseActivity.fromContext(context);
+ mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
+ }
+
+ public void bind(Task task) {
+ mOverlay.reset();
+ mTask = task;
+ int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
+ mPaint.setColor(color);
+ mBackgroundPaint.setColor(color);
+ }
+
+ /**
+ * Updates this thumbnail.
+ */
+ public void setThumbnail(Task task, ThumbnailData thumbnailData) {
+ mTask = task;
+ if (thumbnailData != null && thumbnailData.thumbnail != null) {
+ Bitmap bm = thumbnailData.thumbnail;
+ bm.prepareToDraw();
+ mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ mPaint.setShader(mBitmapShader);
+ mThumbnailData = thumbnailData;
+ updateThumbnailMatrix();
+ } else {
+ mBitmapShader = null;
+ mThumbnailData = null;
+ mPaint.setShader(null);
+ mOverlay.reset();
+ }
+ updateThumbnailPaintFilter();
+ }
+
+ public void setDimAlphaMultipler(float dimAlphaMultipler) {
+ mDimAlphaMultiplier = dimAlphaMultipler;
+ setDimAlpha(mDimAlpha);
+ }
+
+ /**
+ * Sets the alpha of the dim layer on top of this view.
+ * <p>
+ * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black.
+ */
+ public void setDimAlpha(float dimAlpha) {
+ mDimAlpha = dimAlpha;
+ updateThumbnailPaintFilter();
+ }
+
+ public void setSaturation(float saturation) {
+ mSaturation = saturation;
+ updateThumbnailPaintFilter();
+ }
+
+ public float getDimAlpha() {
+ return mDimAlpha;
+ }
+
+ public Rect getInsets(Rect fallback) {
+ if (mThumbnailData != null) {
+ return mThumbnailData.insets;
+ }
+ return fallback;
+ }
+
+ public int getSysUiStatusNavFlags() {
+ if (mThumbnailData != null) {
+ int flags = 0;
+ flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0
+ ? SystemUiController.FLAG_LIGHT_STATUS
+ : SystemUiController.FLAG_DARK_STATUS;
+ flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
+ ? SystemUiController.FLAG_LIGHT_NAV
+ : SystemUiController.FLAG_DARK_NAV;
+ return flags;
+ }
+ return 0;
+ }
+
+ public TaskOverlay getTaskOverlay() {
+ return mOverlay;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ float fullscreenProgress = ((TaskView) getParent()).getFullscreenProgress();
+ if (mIsRotated) {
+ // Don't show insets in the wrong orientation.
+ fullscreenProgress = 0;
+ }
+ if (fullscreenProgress > 0) {
+ // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
+ drawOnCanvas(canvas,
+ -mScaledInsets.left * fullscreenProgress,
+ -mScaledInsets.top * fullscreenProgress,
+ getMeasuredWidth() + mScaledInsets.right * fullscreenProgress,
+ getMeasuredHeight() + mScaledInsets.bottom * fullscreenProgress,
+ mCornerRadius);
+ } else {
+ drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius);
+ }
+ }
+
+ public float getCornerRadius() {
+ return mCornerRadius;
+ }
+
+ public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
+ float cornerRadius) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
+ mDimmingPaintAfterClearing);
+ return;
+ }
+ }
+
+ // Draw the background in all cases, except when the thumbnail data is opaque
+ final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
+ || mThumbnailData == null;
+ if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) {
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
+ if (drawBackgroundOnly) {
+ return;
+ }
+ }
+
+ if (mClipBottom > 0) {
+ canvas.save();
+ canvas.clipRect(x, y, width, mClipBottom);
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
+ canvas.restore();
+ } else {
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
+ }
+ }
+
+ protected TaskView getTaskView() {
+ return (TaskView) getParent();
+ }
+
+ private void updateThumbnailPaintFilter() {
+ int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255);
+ ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation);
+ mBackgroundPaint.setColorFilter(filter);
+ mDimmingPaintAfterClearing.setAlpha(255 - mul);
+ if (mBitmapShader != null) {
+ mPaint.setColorFilter(filter);
+ } else {
+ mPaint.setColorFilter(null);
+ mPaint.setColor(Color.argb(255, mul, mul, mul));
+ }
+ invalidate();
+ }
+
+ private void updateThumbnailMatrix() {
+ mIsRotated = false;
+ mClipBottom = -1;
+ if (mBitmapShader != null && mThumbnailData != null) {
+ float scale = mThumbnailData.scale;
+ Rect thumbnailInsets = mThumbnailData.insets;
+ final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
+ (thumbnailInsets.left + thumbnailInsets.right) * scale;
+ final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
+ (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+
+ final float thumbnailScale;
+ final DeviceProfile profile = mActivity.getDeviceProfile();
+
+ if (getMeasuredWidth() == 0) {
+ // If we haven't measured , skip the thumbnail drawing and only draw the background
+ // color
+ thumbnailScale = 0f;
+ } else {
+ final Configuration configuration =
+ getContext().getResources().getConfiguration();
+ // Rotate the screenshot if not in multi-window mode
+ mIsRotated = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
+ configuration.orientation != mThumbnailData.orientation &&
+ !mActivity.isInMultiWindowMode() &&
+ mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+ // Scale the screenshot to always fit the width of the card.
+ thumbnailScale = mIsRotated
+ ? getMeasuredWidth() / thumbnailHeight
+ : getMeasuredWidth() / thumbnailWidth;
+ }
+
+ mScaledInsets.set(thumbnailInsets);
+ Utilities.scaleRect(mScaledInsets, thumbnailScale);
+
+ if (mIsRotated) {
+ int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1;
+ mMatrix.setRotate(90 * rotationDir);
+ int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top;
+ int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right;
+ mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale);
+ if (rotationDir == -1) {
+ // Crop the right/bottom side of the screenshot rather than left/top
+ float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight();
+ mMatrix.postTranslate(0, -excessHeight);
+ }
+ // Move the screenshot to the thumbnail window (rotation moved it out).
+ if (rotationDir == 1) {
+ mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0);
+ } else {
+ mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth());
+ }
+ } else {
+ mMatrix.setTranslate(-mThumbnailData.insets.left * scale,
+ -mThumbnailData.insets.top * scale);
+ }
+ mMatrix.postScale(thumbnailScale, thumbnailScale);
+ mBitmapShader.setLocalMatrix(mMatrix);
+
+ float bitmapHeight = Math.max((mIsRotated ? thumbnailWidth : thumbnailHeight)
+ * thumbnailScale, 0);
+ if (Math.round(bitmapHeight) < getMeasuredHeight()) {
+ mClipBottom = bitmapHeight;
+ }
+ mPaint.setShader(mBitmapShader);
+ }
+
+ if (mIsRotated) {
+ // The overlay doesn't really work when the screenshot is rotated, so don't add it.
+ mOverlay.reset();
+ } else {
+ mOverlay.setTaskInfo(mTask, mThumbnailData, mMatrix);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateThumbnailMatrix();
+ }
+
+ /**
+ * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 -
+ * leave unchanged.
+ */
+ private static ColorFilter getColorFilter(int intensity, boolean shouldLighten,
+ float saturation) {
+ intensity = Utilities.boundToRange(intensity, 0, 255);
+
+ if (intensity == 255 && saturation == 1) {
+ return null;
+ }
+
+ final float intensityScale = intensity / 255f;
+ COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1);
+
+ if (saturation != 1) {
+ SATURATION_COLOR_MATRIX.setSaturation(saturation);
+ COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX);
+ }
+
+ if (shouldLighten) {
+ final float[] colorArray = COLOR_MATRIX.getArray();
+ final int colorAdd = 255 - intensity;
+ colorArray[4] = colorAdd;
+ colorArray[9] = colorAdd;
+ colorArray[14] = colorAdd;
+ }
+
+ return new ColorMatrixColorFilter(COLOR_MATRIX);
+ }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
new file mode 100644
index 000000000..942214152
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -0,0 +1,613 @@
+/*
+ * 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 android.widget.Toast.LENGTH_SHORT;
+
+import static com.android.launcher3.BaseActivity.fromContext;
+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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+import android.widget.Toast;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.util.ViewPool.Reusable;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.TaskIconCache;
+import com.android.quickstep.TaskOverlayFactory;
+import com.android.quickstep.TaskSystemShortcut;
+import com.android.quickstep.TaskThumbnailCache;
+import com.android.quickstep.TaskUtils;
+import com.android.quickstep.views.RecentsView.PageCallbacks;
+import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A task in the Recents view.
+ */
+public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
+
+ private static final String TAG = TaskView.class.getSimpleName();
+
+ /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
+ private static final TimeInterpolator CURVE_INTERPOLATOR
+ = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
+
+ /**
+ * The alpha of a black scrim on a page in the carousel as it leaves the screen.
+ * In the resting position of the carousel, the adjacent pages have about half this scrim.
+ */
+ public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
+
+ /**
+ * How much to scale down pages near the edge of the screen.
+ */
+ public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
+
+ public static final long SCALE_ICON_DURATION = 120;
+ private static final long DIM_ANIM_DURATION = 700;
+ private static final long TASK_LAUNCH_ANIM_DURATION = 200;
+
+ public static final Property<TaskView, Float> ZOOM_SCALE =
+ new FloatProperty<TaskView>("zoomScale") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setZoomScale(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mZoomScale;
+ }
+ };
+
+ public static final FloatProperty<TaskView> FULLSCREEN_PROGRESS =
+ new FloatProperty<TaskView>("fullscreenProgress") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setFullscreenProgress(v);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mFullscreenProgress;
+ }
+ };
+
+ private static final FloatProperty<TaskView> FOCUS_TRANSITION =
+ new FloatProperty<TaskView>("focusTransition") {
+ @Override
+ public void setValue(TaskView taskView, float v) {
+ taskView.setIconAndDimTransitionProgress(v, false /* invert */);
+ }
+
+ @Override
+ public Float get(TaskView taskView) {
+ return taskView.mFocusTransitionProgress;
+ }
+ };
+
+ private final OnAttachStateChangeListener mTaskMenuStateListener =
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ if (mMenuView != null) {
+ mMenuView.removeOnAttachStateChangeListener(this);
+ mMenuView = null;
+ }
+ }
+ };
+
+ private Task mTask;
+ private TaskThumbnailView mSnapshotView;
+ private TaskMenuView mMenuView;
+ private IconView mIconView;
+ private DigitalWellBeingToast mDigitalWellBeingToast;
+ private float mCurveScale;
+ private float mZoomScale;
+ private float mFullscreenProgress;
+
+ private Animator mIconAndDimAnimator;
+ private float mFocusTransitionProgress = 1;
+
+ private boolean mShowScreenshot;
+
+ // The current background requests to load the task thumbnail and icon
+ private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
+ private TaskIconCache.IconLoadRequest mIconLoadRequest;
+
+ public TaskView(Context context) {
+ this(context, null);
+ }
+
+ public TaskView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setOnClickListener((view) -> {
+ if (getTask() == null) {
+ return;
+ }
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (isRunningTask()) {
+ createLaunchAnimationForRunningTask().start();
+ } else {
+ launchTask(true /* animate */);
+ }
+ } else {
+ launchTask(true /* animate */);
+ }
+
+ fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss(
+ Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
+ TaskUtils.getLaunchComponentKeyForTask(getTask().key));
+ fromContext(context).getStatsLogManager().logTaskLaunch(getRecentsView(),
+ TaskUtils.getLaunchComponentKeyForTask(getTask().key));
+ });
+ setOutlineProvider(new TaskOutlineProvider(getResources()));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mSnapshotView = findViewById(R.id.snapshot);
+ mIconView = findViewById(R.id.icon);
+ mDigitalWellBeingToast = findViewById(R.id.digital_well_being_toast);
+ }
+
+ public TaskMenuView getMenuView() {
+ return mMenuView;
+ }
+
+ /**
+ * Updates this task view to the given {@param task}.
+ */
+ public void bind(Task task) {
+ mTask = task;
+ mSnapshotView.bind(task);
+ }
+
+ public Task getTask() {
+ return mTask;
+ }
+
+ public TaskThumbnailView getThumbnail() {
+ return mSnapshotView;
+ }
+
+ public IconView getIconView() {
+ return mIconView;
+ }
+
+ public TaskOverlayFactory.TaskOverlay getTaskOverlay() {
+ return mSnapshotView.getTaskOverlay();
+ }
+
+ public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
+ final PendingAnimation pendingAnimation =
+ getRecentsView().createTaskLauncherAnimation(this, TASK_LAUNCH_ANIM_DURATION);
+ pendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
+ AnimatorPlaybackController currentAnimation = AnimatorPlaybackController
+ .wrap(pendingAnimation.anim, TASK_LAUNCH_ANIM_DURATION, null);
+ currentAnimation.setEndAction(() -> {
+ pendingAnimation.finish(true, Touch.SWIPE);
+ launchTask(false);
+ });
+ return currentAnimation;
+ }
+
+ public void launchTask(boolean animate) {
+ launchTask(animate, (result) -> {
+ if (!result) {
+ notifyTaskLaunchFailed(TAG);
+ }
+ }, getHandler());
+ }
+
+ public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
+ Handler resultCallbackHandler) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ if (isRunningTask()) {
+ getRecentsView().finishRecentsAnimation(false,
+ () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
+ } else {
+ getRecentsView().takeScreenshotAndFinishRecentsAnimation(true,
+ () -> launchTaskInternal(animate, resultCallback, resultCallbackHandler));
+ }
+ } else {
+ launchTaskInternal(animate, resultCallback, resultCallbackHandler);
+ }
+ }
+
+ private void launchTaskInternal(boolean animate, Consumer<Boolean> resultCallback,
+ Handler resultCallbackHandler) {
+ if (mTask != null) {
+ final ActivityOptions opts;
+ if (animate) {
+ opts = ((BaseDraggingActivity) fromContext(getContext()))
+ .getActivityLaunchOptions(this);
+ ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
+ opts, resultCallback, resultCallbackHandler);
+ } else {
+ opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> {
+ if (resultCallback != null) {
+ // Only post the animation start after the system has indicated that the
+ // transition has started
+ resultCallbackHandler.post(() -> resultCallback.accept(true));
+ }
+ }, resultCallbackHandler);
+ ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
+ opts, (success) -> {
+ if (resultCallback != null && !success) {
+ // If the call to start activity failed, then post the result
+ // immediately, otherwise, wait for the animation start callback
+ // from the activity options above
+ resultCallbackHandler.post(() -> resultCallback.accept(false));
+ }
+ }, resultCallbackHandler);
+ }
+ }
+ }
+
+ public void onTaskListVisibilityChanged(boolean visible) {
+ if (mTask == null) {
+ return;
+ }
+ if (visible) {
+ // These calls are no-ops if the data is already loaded, try and load the high
+ // resolution thumbnail if the state permits
+ RecentsModel model = RecentsModel.INSTANCE.get(getContext());
+ TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
+ TaskIconCache iconCache = model.getIconCache();
+ mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground(mTask,
+ !thumbnailCache.getHighResLoadingState().isEnabled() /* reducedResolution */,
+ (task) -> mSnapshotView.setThumbnail(task, task.thumbnail));
+ mIconLoadRequest = iconCache.updateIconInBackground(mTask,
+ (task) -> {
+ setIcon(task.icon);
+ mDigitalWellBeingToast.initialize(
+ mTask,
+ (saturation, contentDescription) -> {
+ setContentDescription(contentDescription);
+ mSnapshotView.setSaturation(saturation);
+ });
+ });
+ } else {
+ if (mThumbnailLoadRequest != null) {
+ mThumbnailLoadRequest.cancel();
+ }
+ if (mIconLoadRequest != null) {
+ mIconLoadRequest.cancel();
+ }
+ mSnapshotView.setThumbnail(null, null);
+ setIcon(null);
+ }
+ }
+
+ private boolean showTaskMenu() {
+ getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
+ mMenuView = TaskMenuView.showForTask(this);
+ if (mMenuView != null) {
+ mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
+ }
+ return mMenuView != null;
+ }
+
+ private void setIcon(Drawable icon) {
+ if (icon != null) {
+ mIconView.setDrawable(icon);
+ mIconView.setOnClickListener(v -> showTaskMenu());
+ mIconView.setOnLongClickListener(v -> {
+ requestDisallowInterceptTouchEvent(true);
+ return showTaskMenu();
+ });
+ } else {
+ mIconView.setDrawable(null);
+ mIconView.setOnClickListener(null);
+ mIconView.setOnLongClickListener(null);
+ }
+ }
+
+ private void setIconAndDimTransitionProgress(float progress, boolean invert) {
+ if (invert) {
+ progress = 1 - progress;
+ }
+ mFocusTransitionProgress = progress;
+ mSnapshotView.setDimAlphaMultipler(progress);
+ float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
+ float lowerClamp = invert ? 1f - iconScalePercentage : 0;
+ float upperClamp = invert ? 1 : iconScalePercentage;
+ float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp)
+ .getInterpolation(progress);
+ mIconView.setScaleX(scale);
+ mIconView.setScaleY(scale);
+ }
+
+ public void animateIconScaleAndDimIntoView() {
+ if (mIconAndDimAnimator != null) {
+ mIconAndDimAnimator.cancel();
+ }
+ mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1);
+ mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR);
+ mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIconAndDimAnimator = null;
+ }
+ });
+ mIconAndDimAnimator.start();
+ }
+
+ protected void setIconScaleAndDim(float iconScale) {
+ setIconScaleAndDim(iconScale, false);
+ }
+
+ private void setIconScaleAndDim(float iconScale, boolean invert) {
+ if (mIconAndDimAnimator != null) {
+ mIconAndDimAnimator.cancel();
+ }
+ setIconAndDimTransitionProgress(iconScale, invert);
+ }
+
+ private void resetViewTransforms() {
+ setZoomScale(1);
+ setTranslationX(0f);
+ setTranslationY(0f);
+ setTranslationZ(0);
+ setAlpha(1f);
+ setIconScaleAndDim(1);
+ }
+
+ public void resetVisualProperties() {
+ resetViewTransforms();
+ setFullscreenProgress(0);
+ }
+
+ @Override
+ public void onRecycle() {
+ resetViewTransforms();
+ setFullscreenProgress(0);
+ }
+
+ @Override
+ public void onPageScroll(ScrollState scrollState) {
+ float curveInterpolation =
+ CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
+
+ mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
+ setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
+
+ if (mMenuView != null) {
+ mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
+ mMenuView.setScaleX(getScaleX());
+ mMenuView.setScaleY(getScaleY());
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ setPivotX((right - left) * 0.5f);
+ setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+ }
+
+ public static float getCurveScaleForInterpolation(float linearInterpolation) {
+ float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
+ return getCurveScaleForCurveInterpolation(curveInterpolation);
+ }
+
+ private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
+ return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
+ }
+
+ private void setCurveScale(float curveScale) {
+ mCurveScale = curveScale;
+ onScaleChanged();
+ }
+
+ public float getCurveScale() {
+ return mCurveScale;
+ }
+
+ public void setZoomScale(float adjacentScale) {
+ mZoomScale = adjacentScale;
+ onScaleChanged();
+ }
+
+ private void onScaleChanged() {
+ float scale = mCurveScale * mZoomScale;
+ setScaleX(scale);
+ setScaleY(scale);
+ }
+
+ @Override
+ public boolean hasOverlappingRendering() {
+ // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
+ return false;
+ }
+
+ private static final class TaskOutlineProvider extends ViewOutlineProvider {
+
+ private final int mMarginTop;
+ private final float mRadius;
+
+ TaskOutlineProvider(Resources res) {
+ mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+ mRadius = res.getDimension(R.dimen.task_corner_radius);
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, mMarginTop, view.getWidth(),
+ view.getHeight(), mRadius);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+
+ info.addAction(
+ new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task,
+ getContext().getText(R.string.accessibility_close_task)));
+
+ final Context context = getContext();
+ final BaseDraggingActivity activity = fromContext(context);
+ final List<TaskSystemShortcut> shortcuts =
+ mSnapshotView.getTaskOverlay().getEnabledShortcuts(this);
+ final int count = shortcuts.size();
+ for (int i = 0; i < count; ++i) {
+ final TaskSystemShortcut menuOption = shortcuts.get(i);
+ OnClickListener onClickListener = menuOption.getOnClickListener(activity, this);
+ if (onClickListener != null) {
+ info.addAction(menuOption.createAccessibilityAction(context));
+ }
+ }
+
+ if (mDigitalWellBeingToast.getVisibility() == VISIBLE) {
+ info.addAction(
+ new AccessibilityNodeInfo.AccessibilityAction(
+ R.string.accessibility_app_usage_settings,
+ getContext().getText(R.string.accessibility_app_usage_settings)));
+ }
+
+ final RecentsView recentsView = getRecentsView();
+ final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
+ AccessibilityNodeInfo.CollectionItemInfo.obtain(
+ 0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
+ false);
+ info.setCollectionItemInfo(itemInfo);
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (action == R.string.accessibility_close_task) {
+ getRecentsView().dismissTask(this, true /*animateTaskView*/,
+ true /*removeTask*/);
+ return true;
+ }
+
+ if (action == R.string.accessibility_app_usage_settings) {
+ mDigitalWellBeingToast.openAppUsageSettings();
+ return true;
+ }
+
+ final List<TaskSystemShortcut> shortcuts =
+ mSnapshotView.getTaskOverlay().getEnabledShortcuts(this);
+ final int count = shortcuts.size();
+ for (int i = 0; i < count; ++i) {
+ final TaskSystemShortcut menuOption = shortcuts.get(i);
+ if (menuOption.hasHandlerForAction(action)) {
+ OnClickListener onClickListener = menuOption.getOnClickListener(
+ fromContext(getContext()), this);
+ if (onClickListener != null) {
+ onClickListener.onClick(this);
+ }
+ return true;
+ }
+ }
+
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ public RecentsView getRecentsView() {
+ return (RecentsView) getParent();
+ }
+
+ public void notifyTaskLaunchFailed(String tag) {
+ String msg = "Failed to launch task";
+ if (mTask != null) {
+ msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
+ }
+ Log.w(tag, msg);
+ Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
+ }
+
+ /**
+ * Hides the icon and shows insets when this TaskView is about to be shown fullscreen.
+ * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets.
+ */
+ public void setFullscreenProgress(float progress) {
+ if (progress == mFullscreenProgress) {
+ return;
+ }
+ mFullscreenProgress = progress;
+ boolean isFullscreen = mFullscreenProgress > 0;
+ setIconScaleAndDim(progress, true /* invert */);
+ mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
+ setClipChildren(!isFullscreen);
+ setClipToPadding(!isFullscreen);
+ getThumbnail().invalidate();
+ }
+
+ public float getFullscreenProgress() {
+ return mFullscreenProgress;
+ }
+
+ public boolean isRunningTask() {
+ return this == getRecentsView().getRunningTaskView();
+ }
+
+ public void setShowScreenshot(boolean showScreenshot) {
+ mShowScreenshot = showScreenshot;
+ }
+
+ public boolean showScreenshot() {
+ if (!isRunningTask()) {
+ return true;
+ }
+ return mShowScreenshot;
+ }
+}