1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quickstep;
17 
18 import static com.android.launcher3.Utilities.postAsyncCallback;
19 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
20 import static com.android.launcher3.anim.Interpolators.DEACCEL;
21 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
23 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
24 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
25 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG;
26 
27 import android.animation.Animator;
28 import android.annotation.TargetApi;
29 import android.app.ActivityManager.RunningTaskInfo;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.graphics.Point;
33 import android.graphics.PointF;
34 import android.graphics.Rect;
35 import android.graphics.RectF;
36 import android.os.Build;
37 import android.os.Handler;
38 import android.os.Looper;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.animation.Interpolator;
42 
43 import androidx.annotation.UiThread;
44 
45 import com.android.launcher3.BaseDraggingActivity;
46 import com.android.launcher3.DeviceProfile;
47 import com.android.launcher3.InvariantDeviceProfile;
48 import com.android.launcher3.R;
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.anim.AnimationSuccessListener;
51 import com.android.launcher3.anim.AnimatorPlaybackController;
52 import com.android.launcher3.graphics.RotationMode;
53 import com.android.launcher3.util.VibratorWrapper;
54 import com.android.launcher3.views.FloatingIconView;
55 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
56 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
57 import com.android.quickstep.SysUINavigationMode.Mode;
58 import com.android.quickstep.inputconsumers.InputConsumer;
59 import com.android.quickstep.util.ClipAnimationHelper;
60 import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
61 import com.android.quickstep.util.RectFSpringAnim;
62 import com.android.quickstep.util.RemoteAnimationTargetSet;
63 import com.android.quickstep.util.SwipeAnimationTargetSet;
64 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
65 import com.android.quickstep.views.RecentsView;
66 import com.android.quickstep.views.TaskView;
67 import com.android.systemui.shared.system.InputConsumerController;
68 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
69 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
70 
71 import java.util.function.Consumer;
72 
73 /**
74  * Base class for swipe up handler with some utility methods
75  */
76 @TargetApi(Build.VERSION_CODES.Q)
77 public abstract class BaseSwipeUpHandler<T extends BaseDraggingActivity, Q extends RecentsView>
78         implements SwipeAnimationListener {
79 
80     private static final String TAG = "BaseSwipeUpHandler";
81     protected static final Rect TEMP_RECT = new Rect();
82 
83     // Start resisting when swiping past this factor of mTransitionDragLength.
84     private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
85     // This is how far down we can scale down, where 0f is full screen and 1f is recents.
86     private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
87     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
88 
89     // The distance needed to drag to reach the task size in recents.
90     protected int mTransitionDragLength;
91     // How much further we can drag past recents, as a factor of mTransitionDragLength.
92     protected float mDragLengthFactor = 1;
93 
94     protected final Context mContext;
95     protected final OverviewComponentObserver mOverviewComponentObserver;
96     protected final ActivityControlHelper<T> mActivityControlHelper;
97     protected final RecentsModel mRecentsModel;
98     protected final int mRunningTaskId;
99 
100     protected final ClipAnimationHelper mClipAnimationHelper;
101     protected final TransformParams mTransformParams = new TransformParams();
102 
103     protected final Mode mMode;
104 
105     // Shift in the range of [0, 1].
106     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
107     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
108     // visible.
109     protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
110 
111     protected final ActivityInitListener mActivityInitListener;
112     protected final RecentsAnimationWrapper mRecentsAnimationWrapper;
113 
114     protected T mActivity;
115     protected Q mRecentsView;
116     protected DeviceProfile mDp;
117     private final int mPageSpacing;
118 
119     protected Runnable mGestureEndCallback;
120 
121     protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler();
122     protected MultiStateCallback mStateCallback;
123 
124     protected boolean mCanceled;
125     protected int mFinishingRecentsAnimationForNewTaskId = -1;
126 
BaseSwipeUpHandler(Context context, OverviewComponentObserver overviewComponentObserver, RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId)127     protected BaseSwipeUpHandler(Context context,
128             OverviewComponentObserver overviewComponentObserver,
129             RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
130         mContext = context;
131         mOverviewComponentObserver = overviewComponentObserver;
132         mActivityControlHelper = overviewComponentObserver.getActivityControlHelper();
133         mRecentsModel = recentsModel;
134         mActivityInitListener =
135                 mActivityControlHelper.createActivityInitListener(this::onActivityInit);
136         mRunningTaskId = runningTaskId;
137         mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
138                 this::createNewInputProxyHandler);
139         mMode = SysUINavigationMode.getMode(context);
140 
141         mClipAnimationHelper = new ClipAnimationHelper(context);
142         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
143         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
144                 .getDeviceProfile(mContext));
145     }
146 
setStateOnUiThread(int stateFlag)147     protected void setStateOnUiThread(int stateFlag) {
148         if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
149             mStateCallback.setState(stateFlag);
150         } else {
151             postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
152         }
153     }
154 
performHapticFeedback()155     protected void performHapticFeedback() {
156         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
157     }
158 
getRecentsViewDispatcher(RotationMode rotationMode)159     public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
160         return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null;
161     }
162 
163     @UiThread
updateDisplacement(float displacement)164     public void updateDisplacement(float displacement) {
165         // We are moving in the negative x/y direction
166         displacement = -displacement;
167         float shift;
168         if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
169             shift = mDragLengthFactor;
170         } else {
171             float translation = Math.max(displacement, 0);
172             shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
173             if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
174                 float pullbackProgress = Utilities.getProgress(shift,
175                         DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
176                 pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
177                 shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
178                         * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
179             }
180         }
181 
182         mCurrentShift.updateValue(shift);
183     }
184 
setGestureEndCallback(Runnable gestureEndCallback)185     public void setGestureEndCallback(Runnable gestureEndCallback) {
186         mGestureEndCallback = gestureEndCallback;
187     }
188 
getLaunchIntent()189     public abstract Intent getLaunchIntent();
190 
linkRecentsViewScroll()191     protected void linkRecentsViewScroll() {
192         SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> {
193             mTransformParams.setSyncTransactionApplier(applier);
194             mRecentsAnimationWrapper.runOnInit(() ->
195                     mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier));
196         });
197 
198         mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
199             if (moveWindowWithRecentsScroll()) {
200                 updateFinalShift();
201             }
202         });
203         mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper);
204         mRecentsView.setClipAnimationHelper(mClipAnimationHelper);
205     }
206 
startNewTask(int successStateFlag, Consumer<Boolean> resultCallback)207     protected void startNewTask(int successStateFlag, Consumer<Boolean> resultCallback) {
208         // Launch the task user scrolled to (mRecentsView.getNextPage()).
209         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
210             // We finish recents animation inside launchTask() when live tile is enabled.
211             mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
212                     true /* freezeTaskList */);
213         } else {
214             int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
215             mFinishingRecentsAnimationForNewTaskId = taskId;
216             mRecentsAnimationWrapper.finish(true /* toRecents */, () -> {
217                 if (!mCanceled) {
218                     TaskView nextTask = mRecentsView.getTaskView(taskId);
219                     if (nextTask != null) {
220                         nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
221                                 success -> {
222                                     resultCallback.accept(success);
223                                     if (!success) {
224                                         mActivityControlHelper.onLaunchTaskFailed(mActivity);
225                                         nextTask.notifyTaskLaunchFailed(TAG);
226                                     } else {
227                                         mActivityControlHelper.onLaunchTaskSuccess(mActivity);
228                                     }
229                                 }, mMainThreadHandler);
230                     }
231                     setStateOnUiThread(successStateFlag);
232                 }
233                 mCanceled = false;
234                 mFinishingRecentsAnimationForNewTaskId = -1;
235             });
236         }
237         TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true);
238     }
239 
240     @Override
onRecentsAnimationStart(SwipeAnimationTargetSet targetSet)241     public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
242         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
243         final Rect overviewStackBounds;
244         RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
245 
246         if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) {
247             overviewStackBounds = mActivityControlHelper
248                     .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget);
249             dp = dp.getMultiWindowProfile(mContext, new Point(
250                     overviewStackBounds.width(), overviewStackBounds.height()));
251         } else {
252             // If we are not in multi-window mode, home insets should be same as system insets.
253             dp = dp.copy(mContext);
254             overviewStackBounds = getStackBounds(dp);
255         }
256         dp.updateInsets(targetSet.homeContentInsets);
257         dp.updateIsSeascape(mContext);
258         if (runningTaskTarget != null) {
259             mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
260         }
261 
262         mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */);
263         initTransitionEndpoints(dp);
264 
265         mRecentsAnimationWrapper.setController(targetSet);
266     }
267 
getStackBounds(DeviceProfile dp)268     private Rect getStackBounds(DeviceProfile dp) {
269         if (mActivity != null) {
270             int loc[] = new int[2];
271             View rootView = mActivity.getRootView();
272             rootView.getLocationOnScreen(loc);
273             return new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(),
274                     loc[1] + rootView.getHeight());
275         } else {
276             return new Rect(0, 0, dp.widthPx, dp.heightPx);
277         }
278     }
279 
initTransitionEndpoints(DeviceProfile dp)280     protected void initTransitionEndpoints(DeviceProfile dp) {
281         mDp = dp;
282 
283         mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
284                 dp, mContext, TEMP_RECT);
285         if (!dp.isMultiWindowMode) {
286             // When updating the target rect, also update the home bounds since the location on
287             // screen of the launcher window may be stale (position is not updated until first
288             // traversal after the window is resized).  We only do this for non-multiwindow because
289             // we otherwise use the minimized home bounds provided by the system.
290             mClipAnimationHelper.updateHomeBounds(getStackBounds(dp));
291         }
292         mClipAnimationHelper.updateTargetRect(TEMP_RECT);
293         if (mMode == Mode.NO_BUTTON) {
294             // We can drag all the way to the top of the screen.
295             mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
296         }
297     }
298 
299     /**
300      * Return true if the window should be translated horizontally if the recents view scrolls
301      */
moveWindowWithRecentsScroll()302     protected abstract boolean moveWindowWithRecentsScroll();
303 
onActivityInit(final T activity, Boolean alreadyOnHome)304     protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome);
305 
306     /**
307      * Called to create a input proxy for the running task
308      */
309     @UiThread
createNewInputProxyHandler()310     protected abstract InputConsumer createNewInputProxyHandler();
311 
312     /**
313      * Called when the value of {@link #mCurrentShift} changes
314      */
315     @UiThread
updateFinalShift()316     public abstract void updateFinalShift();
317 
318     /**
319      * Called when motion pause is detected
320      */
onMotionPauseChanged(boolean isPaused)321     public abstract void onMotionPauseChanged(boolean isPaused);
322 
323     @UiThread
onGestureStarted()324     public void onGestureStarted() { }
325 
326     @UiThread
onGestureCancelled()327     public abstract void onGestureCancelled();
328 
329     @UiThread
onGestureEnded(float endVelocity, PointF velocity, PointF downPos)330     public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
331 
onConsumerAboutToBeSwitched(SwipeSharedState sharedState)332     public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState);
333 
setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask)334     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
335 
initWhenReady()336     public void initWhenReady() {
337         // Preload the plan
338         mRecentsModel.getTasks(null);
339 
340         mActivityInitListener.register();
341     }
342 
343     /**
344      * Applies the transform on the recents animation without any additional null checks
345      */
applyTransformUnchecked()346     protected void applyTransformUnchecked() {
347         float shift = mCurrentShift.value;
348         float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
349         float offsetScale = getTaskCurveScaleForOffsetX(offsetX,
350                 mClipAnimationHelper.getTargetRect().width());
351         mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale);
352         mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet,
353                 mTransformParams);
354     }
355 
getTaskCurveScaleForOffsetX(float offsetX, float taskWidth)356     private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
357         float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing;
358         float interpolation = Math.min(1, offsetX / distanceToReachEdge);
359         return TaskView.getCurveScaleForInterpolation(interpolation);
360     }
361 
362     /**
363      * Creates an animation that transforms the current app window into the home app.
364      * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
365      * @param homeAnimationFactory The home animation factory.
366      */
createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)367     protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
368             HomeAnimationFactory homeAnimationFactory) {
369         final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
370         final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
371                 mTransformParams.setProgress(startProgress), false /* launcherOnTop */));
372         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
373 
374         final View floatingView = homeAnimationFactory.getFloatingView();
375         final boolean isFloatingIconView = floatingView instanceof FloatingIconView;
376         RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources());
377         if (isFloatingIconView) {
378             FloatingIconView fiv = (FloatingIconView) floatingView;
379             anim.addAnimatorListener(fiv);
380             fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
381         }
382 
383         AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
384 
385         // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
386         // rounding at the end of the animation.
387         float startRadius = mClipAnimationHelper.getCurrentCornerRadius();
388         float endRadius = startRect.width() / 6f;
389 
390         float startTransformProgress = mTransformParams.getProgress();
391         float endTransformProgress = 1;
392 
393         // We want the window alpha to be 0 once this threshold is met, so that the
394         // FolderIconView can be seen morphing into the icon shape.
395         final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
396         anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() {
397 
398             // Alpha interpolates between [1, 0] between progress values [start, end]
399             final float start = 0f;
400             final float end = 0.85f;
401 
402             private float getWindowAlpha(float progress) {
403                 if (progress <= start) {
404                     return 1f;
405                 }
406                 if (progress >= end) {
407                     return 0f;
408                 }
409                 return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
410             }
411 
412             @Override
413             public void onUpdate(RectF currentRect, float progress) {
414                 homeAnim.setPlayFraction(progress);
415 
416                 mTransformParams.setProgress(
417                         Utilities.mapRange(progress, startTransformProgress, endTransformProgress))
418                         .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress));
419                 if (isFloatingIconView) {
420                     mTransformParams.setCornerRadius(endRadius * progress + startRadius
421                             * (1f - progress));
422                 }
423                 mClipAnimationHelper.applyTransform(targetSet, mTransformParams,
424                         false /* launcherOnTop */);
425 
426                 if (isFloatingIconView) {
427                     ((FloatingIconView) floatingView).update(currentRect, 1f, progress,
428                             windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(),
429                             false);
430                 }
431             }
432 
433             @Override
434             public void onCancel() {
435                 if (isFloatingIconView) {
436                     ((FloatingIconView) floatingView).fastFinish();
437                 }
438             }
439         });
440         anim.addAnimatorListener(new AnimationSuccessListener() {
441             @Override
442             public void onAnimationStart(Animator animation) {
443                 homeAnim.dispatchOnStart();
444             }
445 
446             @Override
447             public void onAnimationSuccess(Animator animator) {
448                 homeAnim.getAnimationPlayer().end();
449             }
450         });
451         return anim;
452     }
453 
454     public interface Factory {
455 
newHandler(RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask)456         BaseSwipeUpHandler newHandler(RunningTaskInfo runningTask,
457                 long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask);
458     }
459 
460     protected interface RunningWindowAnim {
end()461         void end();
462 
cancel()463         void cancel();
464 
wrap(Animator animator)465         static RunningWindowAnim wrap(Animator animator) {
466             return new RunningWindowAnim() {
467                 @Override
468                 public void end() {
469                     animator.end();
470                 }
471 
472                 @Override
473                 public void cancel() {
474                     animator.cancel();
475                 }
476             };
477         }
478 
wrap(RectFSpringAnim rectFSpringAnim)479         static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
480             return new RunningWindowAnim() {
481                 @Override
482                 public void end() {
483                     rectFSpringAnim.end();
484                 }
485 
486                 @Override
487                 public void cancel() {
488                     rectFSpringAnim.cancel();
489                 }
490             };
491         }
492     }
493 }
494