1 /*
2  * Copyright (C) 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;
18 
19 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
20 import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
21 
22 import android.app.WindowConfiguration;
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.util.Log;
26 import android.view.RemoteAnimationTarget;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 
31 import com.android.launcher3.statehandlers.DesktopVisibilityController;
32 import com.android.launcher3.util.SplitConfigurationOptions;
33 import com.android.quickstep.util.AnimatorControllerWithResistance;
34 import com.android.quickstep.util.TaskViewSimulator;
35 import com.android.quickstep.util.TransformParams;
36 import com.android.wm.shell.util.SplitBounds;
37 
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Collections;
41 import java.util.List;
42 
43 /**
44  * Glues together the necessary components to animate a remote target using a
45  * {@link TaskViewSimulator}
46  */
47 public class RemoteTargetGluer {
48     private static final String TAG = "RemoteTargetGluer";
49 
50     // This is the default number of handles to create when we don't know how many tasks are running
51     // (e.g. if we're in split screen). Allocate extra for potential tasks overlaid, like volume.
52     private static final int DEFAULT_NUM_HANDLES = 4;
53 
54     private RemoteTargetHandle[] mRemoteTargetHandles;
55     private SplitConfigurationOptions.SplitBounds mSplitBounds;
56 
57     /**
58      * Use this constructor if remote targets are split-screen independent
59      */
RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy, RemoteAnimationTargets targets, boolean forDesktop)60     public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy,
61             RemoteAnimationTargets targets, boolean forDesktop) {
62         init(context, sizingStrategy, targets.apps.length, forDesktop);
63     }
64 
65     /**
66      * Use this constructor if you want the number of handles created to match the number of active
67      * running tasks
68      */
RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy)69     public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy) {
70         DesktopVisibilityController desktopVisibilityController =
71                 LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
72         if (desktopVisibilityController != null) {
73             int visibleTasksCount = desktopVisibilityController.getVisibleDesktopTasksCount();
74             if (visibleTasksCount > 0) {
75                 // Allocate +1 to account for a new task added to the desktop mode
76                 int numHandles = visibleTasksCount + 1;
77                 init(context, sizingStrategy, numHandles, true /* forDesktop */);
78                 return;
79             }
80         }
81 
82         // Assume 2 handles needed for split, scale down as needed later on when we actually
83         // get remote targets
84         init(context, sizingStrategy, DEFAULT_NUM_HANDLES, false /* forDesktop */);
85     }
86 
init(Context context, BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop)87     private void init(Context context, BaseContainerInterface sizingStrategy, int numHandles,
88             boolean forDesktop) {
89         mRemoteTargetHandles = createHandles(context, sizingStrategy, numHandles, forDesktop);
90     }
91 
createHandles(Context context, BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop)92     private RemoteTargetHandle[] createHandles(Context context,
93             BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop) {
94         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
95         for (int i = 0; i < numHandles; i++) {
96             TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
97             tvs.setIsDesktopTask(forDesktop);
98             TransformParams transformParams = new TransformParams();
99             handles[i] = new RemoteTargetHandle(tvs, transformParams);
100         }
101         return handles;
102     }
103 
104     /**
105      * Pairs together {@link TaskViewSimulator}s and {@link TransformParams} into a
106      * {@link RemoteTargetHandle}
107      * Assigns only the apps associated with {@param targets} into their own TaskViewSimulators.
108      * Length of targets.apps should match that of {@link #mRemoteTargetHandles}.
109      *
110      * If split screen may be active when this is called, you might want to use
111      * {@link #assignTargetsForSplitScreen(RemoteAnimationTargets)}
112      */
assignTargets(RemoteAnimationTargets targets)113     public RemoteTargetHandle[] assignTargets(RemoteAnimationTargets targets) {
114         for (int i = 0; i < mRemoteTargetHandles.length; i++) {
115             RemoteAnimationTarget primaryTaskTarget = targets.apps[i];
116             mRemoteTargetHandles[i].mTransformParams.setTargetSet(
117                     createRemoteAnimationTargetsForTarget(targets, Collections.emptyList()));
118             mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null);
119         }
120         return mRemoteTargetHandles;
121     }
122 
123     /**
124      * Calls {@link #assignTargetsForSplitScreen(RemoteAnimationTargets)} with SplitBounds
125      * information specified.
126      */
assignTargetsForSplitScreen(RemoteAnimationTargets targets, SplitConfigurationOptions.SplitBounds splitBounds)127     public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets,
128             SplitConfigurationOptions.SplitBounds splitBounds) {
129         mSplitBounds = splitBounds;
130         return assignTargetsForSplitScreen(targets);
131     }
132 
133     /**
134      * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this assigns the
135      * apps in {@code targets.apps} to the {@link #mRemoteTargetHandles} with index 0 will being
136      * the left/top task, index 1 right/bottom.
137      */
assignTargetsForSplitScreen(RemoteAnimationTargets targets)138     public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets) {
139         resizeRemoteTargetHandles(targets);
140 
141         // If we are in a true split screen case (2 apps running on screen), either:
142         //     a) mSplitBounds was already set (from the clicked GroupedTaskView)
143         //     b) A SplitBounds was passed up from shell (via AbsSwipeUpHandler)
144         // If both of these are null, we are in a 1-app or 1-app-plus-assistant case.
145         if (mSplitBounds == null) {
146             SplitBounds shellSplitBounds = targets.extras.getParcelable(KEY_EXTRA_SPLIT_BOUNDS,
147                     SplitBounds.class);
148             mSplitBounds = convertShellSplitBoundsToLauncher(shellSplitBounds);
149         }
150 
151         boolean containsSplitTargets = mSplitBounds != null;
152         Log.d(TAG, "containsSplitTargets? " + containsSplitTargets + " handleLength: " +
153                 mRemoteTargetHandles.length + " appsLength: " + targets.apps.length);
154 
155         if (mRemoteTargetHandles.length == 1) {
156             // Single fullscreen app
157 
158             // If we're not in split screen, the splitIds count doesn't really matter since we
159             // should always hit this case.
160             mRemoteTargetHandles[0].mTransformParams.setTargetSet(targets);
161             if (targets.apps.length > 0) {
162                 // Unclear why/when target.apps length == 0, but it sure does happen :(
163                 mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(targets.apps[0], null);
164             }
165         } else if (!containsSplitTargets) {
166             // Single App + Assistant
167             for (int i = 0; i < mRemoteTargetHandles.length; i++) {
168                 mRemoteTargetHandles[i].mTransformParams.setTargetSet(targets);
169                 mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(targets.apps[i], null);
170             }
171         } else {
172             // Split apps (+ maybe assistant)
173             RemoteAnimationTarget topLeftTarget = targets.findTask(mSplitBounds.leftTopTaskId);
174             RemoteAnimationTarget bottomRightTarget = targets.findTask(
175                     mSplitBounds.rightBottomTaskId);
176             List<RemoteAnimationTarget> overlayTargets = Arrays.stream(targets.apps).filter(
177                     target -> target.windowConfiguration.getWindowingMode()
178                             != WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW).toList();
179 
180             // remoteTargetHandle[0] denotes topLeft task, so we pass in the bottomRight to exclude,
181             // vice versa
182             mRemoteTargetHandles[0].mTransformParams.setTargetSet(
183                     createRemoteAnimationTargetsForTarget(targets,
184                             Collections.singletonList(bottomRightTarget)));
185             mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(topLeftTarget, mSplitBounds);
186 
187             mRemoteTargetHandles[1].mTransformParams.setTargetSet(
188                     createRemoteAnimationTargetsForTarget(targets,
189                             Collections.singletonList(topLeftTarget)));
190             mRemoteTargetHandles[1].mTaskViewSimulator.setPreview(bottomRightTarget, mSplitBounds);
191 
192             // Set the remaining overlay tasks to be their own TaskViewSimulator as fullscreen tasks
193             if (!overlayTargets.isEmpty()) {
194                 ArrayList<RemoteAnimationTarget> targetsToExclude = new ArrayList<>();
195                 targetsToExclude.add(topLeftTarget);
196                 targetsToExclude.add(bottomRightTarget);
197                 // Start i at 2 to account for top/left and bottom/right split handles already made
198                 for (int i = 2; i < targets.apps.length; i++) {
199                     if (i >= mRemoteTargetHandles.length) {
200                         Log.e(TAG, String.format("Attempting to animate an untracked target"
201                                 + " (%d handles allocated, but %d want to animate)",
202                                 mRemoteTargetHandles.length, targets.apps.length));
203                         break;
204                     }
205                     mRemoteTargetHandles[i].mTransformParams.setTargetSet(
206                             createRemoteAnimationTargetsForTarget(targets, targetsToExclude));
207                     mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(
208                             overlayTargets.get(i - 2));
209                 }
210 
211             }
212         }
213         return mRemoteTargetHandles;
214     }
215 
216     /**
217      * Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this creates distinct
218      * transform params per app in {@code targets.apps} list.
219      */
assignTargetsForDesktop(RemoteAnimationTargets targets)220     public RemoteTargetHandle[] assignTargetsForDesktop(RemoteAnimationTargets targets) {
221         resizeRemoteTargetHandles(targets);
222 
223         for (int i = 0; i < mRemoteTargetHandles.length; i++) {
224             RemoteAnimationTarget primaryTaskTarget = targets.apps[i];
225             List<RemoteAnimationTarget> excludeTargets = Arrays.stream(targets.apps)
226                     .filter(target -> target.taskId != primaryTaskTarget.taskId).toList();
227             mRemoteTargetHandles[i].mTransformParams.setTargetSet(
228                     createRemoteAnimationTargetsForTarget(targets, excludeTargets));
229             mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null);
230         }
231         return mRemoteTargetHandles;
232     }
233 
234     /**
235      * Resize the `mRemoteTargetHandles` array since we assumed initial size, but
236      * `targets.apps` is the ultimate source of truth here
237      */
resizeRemoteTargetHandles(RemoteAnimationTargets targets)238     private void resizeRemoteTargetHandles(RemoteAnimationTargets targets) {
239         long appCount = Arrays.stream(targets.apps)
240                 .filter(app -> app.mode == targets.targetMode)
241                 .count();
242         Log.d(TAG, "appCount: " + appCount + " handleLength: " + mRemoteTargetHandles.length);
243         if (appCount < mRemoteTargetHandles.length) {
244             Log.d(TAG, "resizing handles");
245             RemoteTargetHandle[] newHandles = new RemoteTargetHandle[(int) appCount];
246             System.arraycopy(mRemoteTargetHandles, 0/*src*/, newHandles, 0/*dst*/, (int) appCount);
247             mRemoteTargetHandles = newHandles;
248         }
249     }
250 
getStartBounds(RemoteAnimationTarget target)251     private Rect getStartBounds(RemoteAnimationTarget target) {
252         return target.startBounds == null ? target.screenSpaceBounds : target.startBounds;
253     }
254 
255     /**
256      * Ensures that we aren't excluding ancillary targets such as home/recents
257      *
258      * @param targetsToExclude Will be excluded from the resulting return value.
259      *                        Pass in an empty list to not exclude anything
260      * @return RemoteAnimationTargets where all the app targets from the passed in
261      *         {@code targets} are included except {@code targetsToExclude}
262      */
createRemoteAnimationTargetsForTarget( @onNull RemoteAnimationTargets targets, @NonNull List<RemoteAnimationTarget> targetsToExclude)263     private RemoteAnimationTargets createRemoteAnimationTargetsForTarget(
264             @NonNull RemoteAnimationTargets targets,
265             @NonNull List<RemoteAnimationTarget> targetsToExclude) {
266         ArrayList<RemoteAnimationTarget> targetsToInclude = new ArrayList<>();
267 
268         for (RemoteAnimationTarget targetCompat : targets.unfilteredApps) {
269             boolean skipTarget = false;
270             for (RemoteAnimationTarget excludingTarget : targetsToExclude) {
271                 if (targetCompat == excludingTarget) {
272                     skipTarget = true;
273                     break;
274                 }
275                 if (excludingTarget != null
276                         && excludingTarget.taskInfo != null
277                         && targetCompat.taskInfo != null
278                         && excludingTarget.taskInfo.parentTaskId == targetCompat.taskInfo.taskId) {
279                     // Also exclude corresponding parent task
280                     skipTarget = true;
281                 }
282             }
283             if (skipTarget) {
284                 continue;
285             }
286             targetsToInclude.add(targetCompat);
287         }
288         final RemoteAnimationTarget[] filteredApps = targetsToInclude.toArray(
289                 new RemoteAnimationTarget[0]);
290         return new RemoteAnimationTargets(
291                 filteredApps, targets.wallpapers, targets.nonApps, targets.targetMode);
292     }
293 
294     /**
295      * The object returned by this is may be modified in
296      * {@link #assignTargetsForSplitScreen(RemoteAnimationTargets)}, specifically the length of the
297      * array may be shortened based on the number of RemoteAnimationTargets present.
298      * <p>
299      * This can be accessed at any time, however the count will be more accurate if accessed after
300      * calling one of the respective assignTargets*() methods
301      */
getRemoteTargetHandles()302     public RemoteTargetHandle[] getRemoteTargetHandles() {
303         return mRemoteTargetHandles;
304     }
305 
getSplitBounds()306     public SplitConfigurationOptions.SplitBounds getSplitBounds() {
307         return mSplitBounds;
308     }
309 
310     /**
311      * Container to keep together all the associated objects whose properties need to be updated to
312      * animate a single remote app target
313      */
314     public static class RemoteTargetHandle {
315         private final TaskViewSimulator mTaskViewSimulator;
316         private final TransformParams mTransformParams;
317         @Nullable
318         private AnimatorControllerWithResistance mPlaybackController;
319 
RemoteTargetHandle(TaskViewSimulator taskViewSimulator, TransformParams transformParams)320         public RemoteTargetHandle(TaskViewSimulator taskViewSimulator,
321                 TransformParams transformParams) {
322             mTransformParams = transformParams;
323             mTaskViewSimulator = taskViewSimulator;
324         }
325 
getTaskViewSimulator()326         public TaskViewSimulator getTaskViewSimulator() {
327             return mTaskViewSimulator;
328         }
329 
getTransformParams()330         public TransformParams getTransformParams() {
331             return mTransformParams;
332         }
333 
334         @Nullable
getPlaybackController()335         public AnimatorControllerWithResistance getPlaybackController() {
336             return mPlaybackController;
337         }
338 
setPlaybackController( @ullable AnimatorControllerWithResistance playbackController)339         public void setPlaybackController(
340                 @Nullable AnimatorControllerWithResistance playbackController) {
341             mPlaybackController = playbackController;
342         }
343     }
344 }
345