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.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
20 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
21 
22 import android.annotation.TargetApi;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.os.Build;
28 import android.util.Log;
29 import android.view.MotionEvent;
30 
31 import androidx.annotation.CallSuper;
32 import androidx.annotation.UiThread;
33 
34 import com.android.launcher3.DeviceProfile;
35 import com.android.launcher3.statemanager.StatefulActivity;
36 import com.android.launcher3.testing.TestProtocol;
37 import com.android.launcher3.util.VibratorWrapper;
38 import com.android.launcher3.util.WindowBounds;
39 import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
40 import com.android.quickstep.util.ActiveGestureLog;
41 import com.android.quickstep.util.ActivityInitListener;
42 import com.android.quickstep.util.RectFSpringAnim;
43 import com.android.quickstep.util.SurfaceTransactionApplier;
44 import com.android.quickstep.util.TransformParams;
45 import com.android.quickstep.views.RecentsView;
46 import com.android.quickstep.views.TaskView;
47 import com.android.systemui.shared.recents.model.ThumbnailData;
48 import com.android.systemui.shared.system.InputConsumerController;
49 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
50 
51 import java.util.ArrayList;
52 import java.util.function.Consumer;
53 
54 /**
55  * Base class for swipe up handler with some utility methods
56  */
57 @TargetApi(Build.VERSION_CODES.Q)
58 public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
59         extends SwipeUpAnimationLogic implements RecentsAnimationListener {
60 
61     private static final String TAG = "BaseSwipeUpHandler";
62 
63     protected final BaseActivityInterface<?, T> mActivityInterface;
64     protected final InputConsumerController mInputConsumer;
65 
66     protected final ActivityInitListener mActivityInitListener;
67 
68     protected RecentsAnimationController mRecentsAnimationController;
69     protected RecentsAnimationTargets mRecentsAnimationTargets;
70 
71     // Callbacks to be made once the recents animation starts
72     private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
73 
74     protected T mActivity;
75     protected Q mRecentsView;
76 
77     protected Runnable mGestureEndCallback;
78 
79     protected MultiStateCallback mStateCallback;
80 
81     protected boolean mCanceled;
82 
83     private boolean mRecentsViewScrollLinked = false;
84 
BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState, GestureState gestureState, InputConsumerController inputConsumer)85     protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
86             GestureState gestureState, InputConsumerController inputConsumer) {
87         super(context, deviceState, gestureState, new TransformParams());
88         mActivityInterface = gestureState.getActivityInterface();
89         mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
90         mInputConsumer = inputConsumer;
91     }
92 
93     /**
94      * To be called at the end of constructor of subclasses. This calls various methods which can
95      * depend on proper class initialization.
96      */
initAfterSubclassConstructor()97     protected void initAfterSubclassConstructor() {
98         initTransitionEndpoints(
99                 mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
100     }
101 
performHapticFeedback()102     protected void performHapticFeedback() {
103         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
104     }
105 
getRecentsViewDispatcher(float navbarRotation)106     public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
107         return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
108     }
109 
setGestureEndCallback(Runnable gestureEndCallback)110     public void setGestureEndCallback(Runnable gestureEndCallback) {
111         mGestureEndCallback = gestureEndCallback;
112     }
113 
getLaunchIntent()114     public abstract Intent getLaunchIntent();
115 
linkRecentsViewScroll()116     protected void linkRecentsViewScroll() {
117         SurfaceTransactionApplier.create(mRecentsView, applier -> {
118             mTransformParams.setSyncTransactionApplier(applier);
119             runOnRecentsAnimationStart(() ->
120                     mRecentsAnimationTargets.addReleaseCheck(applier));
121         });
122 
123         mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
124             if (moveWindowWithRecentsScroll()) {
125                 updateFinalShift();
126             }
127         });
128         runOnRecentsAnimationStart(() ->
129                 mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
130                         mRecentsAnimationTargets));
131         mRecentsViewScrollLinked = true;
132     }
133 
startNewTask(Consumer<Boolean> resultCallback)134     protected void startNewTask(Consumer<Boolean> resultCallback) {
135         // Launch the task user scrolled to (mRecentsView.getNextPage()).
136         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
137             // We finish recents animation inside launchTask() when live tile is enabled.
138             mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
139                     true /* freezeTaskList */);
140         } else {
141             int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
142             if (!mCanceled) {
143                 TaskView nextTask = mRecentsView.getTaskView(taskId);
144                 if (nextTask != null) {
145                     mGestureState.updateLastStartedTaskId(taskId);
146                     boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
147                             .contains(taskId);
148                     nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
149                             success -> {
150                                 resultCallback.accept(success);
151                                 if (success) {
152                                     if (hasTaskPreviouslyAppeared) {
153                                         onRestartPreviouslyAppearedTask();
154                                     }
155                                 } else {
156                                     mActivityInterface.onLaunchTaskFailed();
157                                     nextTask.notifyTaskLaunchFailed(TAG);
158                                     mRecentsAnimationController.finish(true /* toRecents */, null);
159                                 }
160                             }, MAIN_EXECUTOR.getHandler());
161                 }
162             }
163             mCanceled = false;
164         }
165     }
166 
167     /**
168      * Called when we successfully startNewTask() on the task that was previously running. Normally
169      * we call resumeLastTask() when returning to the previously running task, but this handles a
170      * specific edge case: if we switch from A to B, and back to A before B appears, we need to
171      * start A again to ensure it stays on top.
172      */
173     @CallSuper
onRestartPreviouslyAppearedTask()174     protected void onRestartPreviouslyAppearedTask() {
175         // Finish the controller here, since we won't get onTaskAppeared() for a task that already
176         // appeared.
177         if (mRecentsAnimationController != null) {
178             mRecentsAnimationController.finish(false, null);
179         }
180     }
181 
182     /**
183      * Runs the given {@param action} if the recents animation has already started, or queues it to
184      * be run when it is next started.
185      */
runOnRecentsAnimationStart(Runnable action)186     protected void runOnRecentsAnimationStart(Runnable action) {
187         if (mRecentsAnimationTargets == null) {
188             mRecentsAnimationStartCallbacks.add(action);
189         } else {
190             action.run();
191         }
192     }
193 
194     /**
195      * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
196      * @return whether the recents animation has started and there are valid app targets.
197      */
hasTargets()198     protected boolean hasTargets() {
199         return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
200     }
201 
202     @Override
onRecentsAnimationStart(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets targets)203     public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
204             RecentsAnimationTargets targets) {
205         mRecentsAnimationController = recentsAnimationController;
206         mRecentsAnimationTargets = targets;
207         mTransformParams.setTargetSet(mRecentsAnimationTargets);
208         RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
209                 mGestureState.getRunningTaskId());
210 
211         if (runningTaskTarget != null) {
212             mTaskViewSimulator.setPreview(runningTaskTarget);
213         }
214 
215         // Only initialize the device profile, if it has not been initialized before, as in some
216         // configurations targets.homeContentInsets may not be correct.
217         if (mActivity == null) {
218             DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
219             if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
220                 Rect overviewStackBounds = mActivityInterface
221                         .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
222                 dp = dp.getMultiWindowProfile(mContext,
223                         new WindowBounds(overviewStackBounds, targets.homeContentInsets));
224             } else {
225                 // If we are not in multi-window mode, home insets should be same as system insets.
226                 dp = dp.copy(mContext);
227             }
228             dp.updateInsets(targets.homeContentInsets);
229             dp.updateIsSeascape(mContext);
230             initTransitionEndpoints(dp);
231         }
232 
233         // Notify when the animation starts
234         if (!mRecentsAnimationStartCallbacks.isEmpty()) {
235             for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
236                 action.run();
237             }
238             mRecentsAnimationStartCallbacks.clear();
239         }
240     }
241 
242     @Override
onRecentsAnimationCanceled(ThumbnailData thumbnailData)243     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
244         mRecentsAnimationController = null;
245         mRecentsAnimationTargets = null;
246         if (mRecentsView != null) {
247             mRecentsView.setRecentsAnimationTargets(null, null);
248         }
249     }
250 
251     @Override
onRecentsAnimationFinished(RecentsAnimationController controller)252     public void onRecentsAnimationFinished(RecentsAnimationController controller) {
253         mRecentsAnimationController = null;
254         mRecentsAnimationTargets = null;
255         if (mRecentsView != null) {
256             mRecentsView.setRecentsAnimationTargets(null, null);
257         }
258     }
259 
260     @Override
onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget)261     public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
262         if (mRecentsAnimationController != null) {
263             if (handleTaskAppeared(appearedTaskTarget)) {
264                 mRecentsAnimationController.finish(false /* toRecents */,
265                         null /* onFinishComplete */);
266                 mActivityInterface.onLaunchTaskSuccess();
267                 ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
268             }
269         }
270     }
271 
272     /** @return Whether this was the task we were waiting to appear, and thus handled it. */
handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget)273     protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
274 
275     /**
276      * @return The index of the TaskView in RecentsView whose taskId matches the task that will
277      * resume if we finish the controller.
278      */
getLastAppearedTaskIndex()279     protected int getLastAppearedTaskIndex() {
280         return mGestureState.getLastAppearedTaskId() != -1
281                 ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
282                 : mRecentsView.getRunningTaskIndex();
283     }
284 
285     /**
286      * @return Whether we are continuing a gesture that already landed on a new task,
287      * but before that task appeared.
288      */
hasStartedNewTask()289     protected boolean hasStartedNewTask() {
290         return mGestureState.getLastStartedTaskId() != -1;
291     }
292 
293     /**
294      * Return true if the window should be translated horizontally if the recents view scrolls
295      */
moveWindowWithRecentsScroll()296     protected abstract boolean moveWindowWithRecentsScroll();
297 
onActivityInit(Boolean alreadyOnHome)298     protected boolean onActivityInit(Boolean alreadyOnHome) {
299         T createdActivity = mActivityInterface.getCreatedActivity();
300         if (TestProtocol.sDebugTracing) {
301             Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
302         }
303         if (createdActivity != null) {
304             if (TestProtocol.sDebugTracing) {
305                 Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
306             }
307             initTransitionEndpoints(createdActivity.getDeviceProfile());
308         }
309         return true;
310     }
311 
312     /**
313      * Called to create a input proxy for the running task
314      */
315     @UiThread
createNewInputProxyHandler()316     protected abstract InputConsumer createNewInputProxyHandler();
317 
318     /**
319      * Called when the value of {@link #mCurrentShift} changes
320      */
321     @UiThread
updateFinalShift()322     public abstract void updateFinalShift();
323 
324     /**
325      * Called when motion pause is detected
326      */
onMotionPauseChanged(boolean isPaused)327     public abstract void onMotionPauseChanged(boolean isPaused);
328 
329     @UiThread
onGestureStarted(boolean isLikelyToStartNewTask)330     public void onGestureStarted(boolean isLikelyToStartNewTask) { }
331 
332     @UiThread
onGestureCancelled()333     public abstract void onGestureCancelled();
334 
335     @UiThread
onGestureEnded(float endVelocity, PointF velocity, PointF downPos)336     public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
337 
onConsumerAboutToBeSwitched()338     public abstract void onConsumerAboutToBeSwitched();
339 
setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask)340     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
341 
342     /**
343      * Registers a callback to run when the activity is ready.
344      * @param intent The intent that will be used to start the activity if it doesn't exist already.
345      */
initWhenReady(Intent intent)346     public void initWhenReady(Intent intent) {
347         // Preload the plan
348         RecentsModel.INSTANCE.get(mContext).getTasks(null);
349 
350         mActivityInitListener.register(intent);
351     }
352 
353     /**
354      * Applies the transform on the recents animation
355      */
applyWindowTransform()356     protected void applyWindowTransform() {
357         if (mWindowTransitionController != null) {
358             float progress = mCurrentShift.value / mDragLengthFactor;
359             mWindowTransitionController.setPlayFraction(progress);
360         }
361         if (mRecentsAnimationTargets != null) {
362             if (mRecentsViewScrollLinked) {
363                 mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
364             }
365             mTaskViewSimulator.apply(mTransformParams);
366         }
367     }
368 
369     @Override
createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)370     protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
371             HomeAnimationFactory homeAnimationFactory) {
372         RectFSpringAnim anim =
373                 super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
374         if (mRecentsAnimationTargets != null) {
375             mRecentsAnimationTargets.addReleaseCheck(anim);
376         }
377         return anim;
378     }
379 
380     public interface Factory {
381 
newHandler( GestureState gestureState, long touchTimeMs, boolean continuingLastGesture)382         BaseSwipeUpHandler newHandler(
383                 GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
384     }
385 }
386