1 /*
2  * Copyright 2021 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 
17 package com.android.quickstep.util;
18 
19 import static com.android.launcher3.Utilities.postAsyncCallback;
20 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
21 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
22 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
23 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
24 
25 import android.app.ActivityOptions;
26 import android.app.ActivityThread;
27 import android.graphics.Rect;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.view.RemoteAnimationAdapter;
31 import android.view.SurfaceControl;
32 import android.window.TransitionInfo;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.launcher3.statehandlers.DepthController;
37 import com.android.launcher3.statemanager.StateManager;
38 import com.android.launcher3.util.SplitConfigurationOptions;
39 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
40 import com.android.quickstep.SystemUiProxy;
41 import com.android.quickstep.TaskAnimationManager;
42 import com.android.quickstep.TaskViewUtils;
43 import com.android.quickstep.views.GroupedTaskView;
44 import com.android.quickstep.views.TaskView;
45 import com.android.systemui.shared.recents.model.Task;
46 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
47 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
48 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
49 import com.android.systemui.shared.system.RemoteTransitionCompat;
50 import com.android.systemui.shared.system.RemoteTransitionRunner;
51 
52 import java.util.function.Consumer;
53 
54 /**
55  * Represent data needed for the transient state when user has selected one app for split screen
56  * and is in the process of either a) selecting a second app or b) exiting intention to invoke split
57  */
58 public class SplitSelectStateController {
59 
60     private final Handler mHandler;
61     private final SystemUiProxy mSystemUiProxy;
62     private final StateManager mStateManager;
63     private final DepthController mDepthController;
64     private @StagePosition int mStagePosition;
65     private Task mInitialTask;
66     private Task mSecondTask;
67     private boolean mRecentsAnimationRunning;
68     /** If not null, this is the TaskView we want to launch from */
69     @Nullable
70     private GroupedTaskView mLaunchingTaskView;
71 
SplitSelectStateController(Handler handler, SystemUiProxy systemUiProxy, StateManager stateManager, DepthController depthController)72     public SplitSelectStateController(Handler handler, SystemUiProxy systemUiProxy,
73             StateManager stateManager,
74             DepthController depthController) {
75         mHandler = handler;
76         mSystemUiProxy = systemUiProxy;
77         mStateManager = stateManager;
78         mDepthController = depthController;
79     }
80 
81     /**
82      * To be called after first task selected
83      */
setInitialTaskSelect(Task task, @StagePosition int stagePosition, Rect initialBounds)84     public void setInitialTaskSelect(Task task, @StagePosition int stagePosition,
85             Rect initialBounds) {
86         mInitialTask = task;
87         mStagePosition = stagePosition;
88     }
89 
90     /**
91      * To be called after second task selected
92      */
setSecondTaskId(Task task, Consumer<Boolean> callback)93     public void setSecondTaskId(Task task, Consumer<Boolean> callback) {
94         mSecondTask = task;
95         launchTasks(mInitialTask, mSecondTask, mStagePosition, callback,
96                 false /* freezeTaskList */, DEFAULT_SPLIT_RATIO);
97     }
98 
99     /**
100      * To be called when we want to launch split pairs from an existing GroupedTaskView.
101      */
launchTasks(GroupedTaskView groupedTaskView, Consumer<Boolean> callback, boolean freezeTaskList)102     public void launchTasks(GroupedTaskView groupedTaskView,
103             Consumer<Boolean> callback, boolean freezeTaskList) {
104         mLaunchingTaskView = groupedTaskView;
105         TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
106                 groupedTaskView.getTaskIdAttributeContainers();
107         launchTasks(taskIdAttributeContainers[0].getTask(), taskIdAttributeContainers[1].getTask(),
108                 taskIdAttributeContainers[0].getStagePosition(), callback, freezeTaskList,
109                 groupedTaskView.getSplitRatio());
110     }
111 
112     /**
113      * @param stagePosition representing location of task1
114      */
launchTasks(Task task1, Task task2, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio)115     public void launchTasks(Task task1, Task task2, @StagePosition int stagePosition,
116             Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
117         // Assume initial task is for top/left part of screen
118         final int[] taskIds = stagePosition == STAGE_POSITION_TOP_OR_LEFT
119                 ? new int[]{task1.key.id, task2.key.id}
120                 : new int[]{task2.key.id, task1.key.id};
121         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
122             RemoteSplitLaunchTransitionRunner animationRunner =
123                     new RemoteSplitLaunchTransitionRunner(task1, task2);
124             mSystemUiProxy.startTasks(taskIds[0], null /* mainOptions */, taskIds[1],
125                     null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT, splitRatio,
126                     new RemoteTransitionCompat(animationRunner, MAIN_EXECUTOR,
127                             ActivityThread.currentActivityThread().getApplicationThread()));
128         } else {
129             RemoteSplitLaunchAnimationRunner animationRunner =
130                     new RemoteSplitLaunchAnimationRunner(task1, task2, callback);
131             final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
132                     RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner),
133                     300, 150,
134                     ActivityThread.currentActivityThread().getApplicationThread());
135 
136             ActivityOptions mainOpts = ActivityOptions.makeBasic();
137             if (freezeTaskList) {
138                 mainOpts.setFreezeRecentTasksReordering();
139             }
140             mSystemUiProxy.startTasksWithLegacyTransition(taskIds[0], mainOpts.toBundle(),
141                     taskIds[1], null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT,
142                     splitRatio, adapter);
143         }
144     }
145 
getActiveSplitStagePosition()146     public @StagePosition int getActiveSplitStagePosition() {
147         return mStagePosition;
148     }
149 
setRecentsAnimationRunning(boolean running)150     public void setRecentsAnimationRunning(boolean running) {
151         this.mRecentsAnimationRunning = running;
152     }
153 
154     /**
155      * Requires Shell Transitions
156      */
157     private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner {
158 
159         private final Task mInitialTask;
160         private final Task mSecondTask;
161 
RemoteSplitLaunchTransitionRunner(Task initialTask, Task secondTask)162         RemoteSplitLaunchTransitionRunner(Task initialTask, Task secondTask) {
163             mInitialTask = initialTask;
164             mSecondTask = secondTask;
165         }
166 
167         @Override
startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, Runnable finishCallback)168         public void startAnimation(IBinder transition, TransitionInfo info,
169                 SurfaceControl.Transaction t, Runnable finishCallback) {
170             TaskViewUtils.composeRecentsSplitLaunchAnimator(mInitialTask,
171                     mSecondTask, info, t, finishCallback);
172             // After successful launch, call resetState
173             resetState();
174         }
175     }
176 
177     /**
178      * LEGACY
179      * Remote animation runner for animation to launch an app.
180      */
181     private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat {
182 
183         private final Task mInitialTask;
184         private final Task mSecondTask;
185         private final Consumer<Boolean> mSuccessCallback;
186 
RemoteSplitLaunchAnimationRunner(Task initialTask, Task secondTask, Consumer<Boolean> successCallback)187         RemoteSplitLaunchAnimationRunner(Task initialTask, Task secondTask,
188                 Consumer<Boolean> successCallback) {
189             mInitialTask = initialTask;
190             mSecondTask = secondTask;
191             mSuccessCallback = successCallback;
192         }
193 
194         @Override
onAnimationStart(int transit, RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback)195         public void onAnimationStart(int transit, RemoteAnimationTargetCompat[] apps,
196                 RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps,
197                 Runnable finishedCallback) {
198             postAsyncCallback(mHandler,
199                     () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
200                             mLaunchingTaskView, mInitialTask, mSecondTask, apps, wallpapers,
201                             nonApps, mStateManager, mDepthController, () -> {
202                                 finishedCallback.run();
203                                 if (mSuccessCallback != null) {
204                                     mSuccessCallback.accept(true);
205                                 }
206                                 resetState();
207                             }));
208         }
209 
210         @Override
onAnimationCancelled()211         public void onAnimationCancelled() {
212             postAsyncCallback(mHandler, () -> {
213                 if (mSuccessCallback != null) {
214                     // Launching legacy tasks while recents animation is running will always cause
215                     // onAnimationCancelled to be called (should be fixed w/ shell transitions?)
216                     mSuccessCallback.accept(mRecentsAnimationRunning);
217                 }
218                 resetState();
219             });
220         }
221     }
222 
223     /**
224      * To be called if split select was cancelled
225      */
resetState()226     public void resetState() {
227         mInitialTask = null;
228         mSecondTask = null;
229         mStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
230         mRecentsAnimationRunning = false;
231         mLaunchingTaskView = null;
232     }
233 
234     /**
235      * @return {@code true} if first task has been selected and waiting for the second task to be
236      *         chosen
237      */
isSplitSelectActive()238     public boolean isSplitSelectActive() {
239         return mInitialTask != null && mSecondTask == null;
240     }
241 }
242