1 /*
2  * Copyright (C) 2023 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.wm.shell.util;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.view.RemoteAnimationTarget.MODE_CHANGING;
21 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
22 import static android.view.RemoteAnimationTarget.MODE_OPENING;
23 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
24 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
25 import static android.view.WindowManager.TRANSIT_CHANGE;
26 import static android.view.WindowManager.TRANSIT_CLOSE;
27 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
28 import static android.view.WindowManager.TRANSIT_OPEN;
29 import static android.view.WindowManager.TRANSIT_TO_BACK;
30 import static android.view.WindowManager.TRANSIT_TO_FRONT;
31 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
32 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
33 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
34 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
35 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
36 
37 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
38 
39 import android.annotation.NonNull;
40 import android.annotation.Nullable;
41 import android.annotation.SuppressLint;
42 import android.app.ActivityManager;
43 import android.app.WindowConfiguration;
44 import android.graphics.Rect;
45 import android.util.ArrayMap;
46 import android.util.SparseBooleanArray;
47 import android.view.RemoteAnimationTarget;
48 import android.view.SurfaceControl;
49 import android.view.WindowManager;
50 import android.window.TransitionInfo;
51 
52 import java.util.function.Predicate;
53 
54 /** Various utility functions for transitions. */
55 public class TransitionUtil {
56 
57     /** @return true if the transition was triggered by opening something vs closing something */
isOpeningType(@indowManager.TransitionType int type)58     public static boolean isOpeningType(@WindowManager.TransitionType int type) {
59         return type == TRANSIT_OPEN
60                 || type == TRANSIT_TO_FRONT
61                 || type == TRANSIT_KEYGUARD_GOING_AWAY;
62     }
63 
64     /** @return true if the transition was triggered by closing something vs opening something */
isClosingType(@indowManager.TransitionType int type)65     public static boolean isClosingType(@WindowManager.TransitionType int type) {
66         return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
67     }
68 
69     /** Returns {@code true} if the transition is opening or closing mode. */
isOpenOrCloseMode(@ransitionInfo.TransitionMode int mode)70     public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
71         return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE
72                 || mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK;
73     }
74 
75     /** Returns {@code true} if the transition has a display change. */
hasDisplayChange(@onNull TransitionInfo info)76     public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
77         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
78             final TransitionInfo.Change change = info.getChanges().get(i);
79             if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
80                 return true;
81             }
82         }
83         return false;
84     }
85 
86     /** Returns `true` if `change` is a wallpaper. */
isWallpaper(TransitionInfo.Change change)87     public static boolean isWallpaper(TransitionInfo.Change change) {
88         return (change.getTaskInfo() == null)
89                 && change.hasFlags(FLAG_IS_WALLPAPER)
90                 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
91     }
92 
93     /** Returns `true` if `change` is not an app window or wallpaper. */
isNonApp(TransitionInfo.Change change)94     public static boolean isNonApp(TransitionInfo.Change change) {
95         return (change.getTaskInfo() == null)
96                 && !change.hasFlags(FLAG_IS_WALLPAPER)
97                 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
98     }
99 
100     /** Returns `true` if `change` is the divider. */
isDividerBar(TransitionInfo.Change change)101     public static boolean isDividerBar(TransitionInfo.Change change) {
102         return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
103     }
104 
105     /** Returns `true` if `change` is only re-ordering. */
isOrderOnly(TransitionInfo.Change change)106     public static boolean isOrderOnly(TransitionInfo.Change change) {
107         return change.getMode() == TRANSIT_CHANGE
108                 && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0
109                 && change.getStartAbsBounds().equals(change.getEndAbsBounds())
110                 && (change.getLastParent() == null
111                         || change.getLastParent().equals(change.getParent()));
112     }
113 
114     /**
115      * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
116      * MUST call `test` in the same order that the changes appear in the TransitionInfo.
117      */
118     public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> {
119         private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray();
120 
121         @Override
test(TransitionInfo.Change change)122         public boolean test(TransitionInfo.Change change) {
123             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
124             if (taskInfo == null) return false;
125             // Children always come before parent since changes are in top-to-bottom z-order.
126             boolean hasChildren = mChildTaskTargets.get(taskInfo.taskId);
127             if (taskInfo.hasParentTask()) {
128                 mChildTaskTargets.put(taskInfo.parentTaskId, true);
129             }
130             // If it has children, it's not a leaf.
131             return !hasChildren;
132         }
133     }
134 
135 
newModeToLegacyMode(int newMode)136     private static int newModeToLegacyMode(int newMode) {
137         switch (newMode) {
138             case WindowManager.TRANSIT_OPEN:
139             case WindowManager.TRANSIT_TO_FRONT:
140                 return MODE_OPENING;
141             case WindowManager.TRANSIT_CLOSE:
142             case WindowManager.TRANSIT_TO_BACK:
143                 return MODE_CLOSING;
144             default:
145                 return MODE_CHANGING;
146         }
147     }
148 
149     /**
150      * Very similar to Transitions#setupAnimHierarchy but specialized for leashes.
151      */
152     @SuppressLint("NewApi")
setupLeash(@onNull SurfaceControl leash, @NonNull TransitionInfo.Change change, int layer, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t)153     private static void setupLeash(@NonNull SurfaceControl leash,
154             @NonNull TransitionInfo.Change change, int layer,
155             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
156         final boolean isOpening = TransitionUtil.isOpeningType(info.getType());
157         // Put animating stuff above this line and put static stuff below it.
158         int zSplitLine = info.getChanges().size();
159         // changes should be ordered top-to-bottom in z
160         final int mode = change.getMode();
161 
162         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
163         t.reparent(leash, info.getRoot(rootIdx).getLeash());
164         final Rect absBounds =
165                 (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
166         t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
167                 absBounds.top - info.getRoot(rootIdx).getOffset().y);
168 
169         if (isDividerBar(change)) {
170             if (isOpeningType(mode)) {
171                 t.setAlpha(leash, 0.f);
172             }
173             // Set the transition leash position to 0 in case the divider leash position being
174             // taking down.
175             t.setPosition(leash, 0, 0);
176             t.setLayer(leash, Integer.MAX_VALUE);
177             return;
178         }
179 
180         // Put all the OPEN/SHOW on top
181         if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
182             // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
183             if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
184                 t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer);
185             } else {
186                 t.setLayer(leash, -zSplitLine - layer);
187             }
188         } else if (TransitionUtil.isOpeningType(mode)) {
189             if (isOpening) {
190                 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
191                 if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
192                     // if transferred, it should be left visible.
193                     t.setAlpha(leash, 0.f);
194                 }
195             } else {
196                 // put on bottom and leave it visible
197                 t.setLayer(leash, zSplitLine - layer);
198             }
199         } else if (TransitionUtil.isClosingType(mode)) {
200             if (isOpening) {
201                 // put on bottom and leave visible
202                 t.setLayer(leash, zSplitLine - layer);
203             } else {
204                 // put on top
205                 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
206             }
207         } else { // CHANGE
208             t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
209         }
210     }
211 
212     @SuppressLint("NewApi")
createLeash(TransitionInfo info, TransitionInfo.Change change, int order, SurfaceControl.Transaction t)213     private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change,
214             int order, SurfaceControl.Transaction t) {
215         // TODO: once we can properly sync transactions across process, then get rid of this leash.
216         if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
217             // Special case for wallpaper atm. Normally these are left alone; but, a quirk of
218             // making leashes means we have to handle them specially.
219             return change.getLeash();
220         }
221         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
222         SurfaceControl leashSurface = new SurfaceControl.Builder()
223                 .setName(change.getLeash().toString() + "_transition-leash")
224                 .setContainerLayer()
225                 // Initial the surface visible to respect the visibility of the original surface.
226                 .setHidden(false)
227                 .setParent(info.getRoot(rootIdx).getLeash())
228                 .build();
229         // Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
230         setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
231         t.reparent(change.getLeash(), leashSurface);
232         t.setAlpha(change.getLeash(), 1.0f);
233         t.show(change.getLeash());
234         if (!isDividerBar(change)) {
235             // For divider, don't modify its inner leash position when creating the outer leash
236             // for the transition. In case the position being wrong after the transition finished.
237             t.setPosition(change.getLeash(), 0, 0);
238         }
239         t.setLayer(change.getLeash(), 0);
240         return leashSurface;
241     }
242 
243     /**
244      * Creates a new RemoteAnimationTarget from the provided change info
245      */
newTarget(TransitionInfo.Change change, int order, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)246     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
247             TransitionInfo info, SurfaceControl.Transaction t,
248             @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
249         return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap);
250     }
251 
252     /**
253      * Creates a new RemoteAnimationTarget from the provided change info
254      */
newTarget(TransitionInfo.Change change, int order, boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)255     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
256             boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t,
257             @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
258         final SurfaceControl leash = createLeash(info, change, order, t);
259         if (leashMap != null) {
260             leashMap.put(change.getLeash(), leash);
261         }
262         return newTarget(change, order, leash, forceTranslucent);
263     }
264 
265     /**
266      * Creates a new RemoteAnimationTarget from the provided change and leash
267      */
newTarget(TransitionInfo.Change change, int order, SurfaceControl leash)268     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
269             SurfaceControl leash) {
270         return newTarget(change, order, leash, false /* forceTranslucent */);
271     }
272 
273     /**
274      * Creates a new RemoteAnimationTarget from the provided change and leash
275      */
newTarget(TransitionInfo.Change change, int order, SurfaceControl leash, boolean forceTranslucent)276     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
277             SurfaceControl leash, boolean forceTranslucent) {
278         if (isDividerBar(change)) {
279             return getDividerTarget(change, leash);
280         }
281 
282         int taskId;
283         boolean isNotInRecents;
284         ActivityManager.RunningTaskInfo taskInfo;
285         WindowConfiguration windowConfiguration;
286 
287         taskInfo = change.getTaskInfo();
288         if (taskInfo != null) {
289             taskId = taskInfo.taskId;
290             isNotInRecents = !taskInfo.isRunning;
291             windowConfiguration = taskInfo.configuration.windowConfiguration;
292         } else {
293             taskId = INVALID_TASK_ID;
294             isNotInRecents = true;
295             windowConfiguration = new WindowConfiguration();
296         }
297 
298         Rect localBounds = new Rect(change.getEndAbsBounds());
299         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
300 
301         RemoteAnimationTarget target = new RemoteAnimationTarget(
302                 taskId,
303                 newModeToLegacyMode(change.getMode()),
304                 // TODO: once we can properly sync transactions across process,
305                 // then get rid of this leash.
306                 leash,
307                 forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
308                 null,
309                 // TODO(shell-transitions): we need to send content insets? evaluate how its used.
310                 new Rect(0, 0, 0, 0),
311                 order,
312                 null,
313                 localBounds,
314                 new Rect(change.getEndAbsBounds()),
315                 windowConfiguration,
316                 isNotInRecents,
317                 null,
318                 new Rect(change.getStartAbsBounds()),
319                 taskInfo,
320                 change.getAllowEnterPip(),
321                 INVALID_WINDOW_TYPE
322         );
323         target.setWillShowImeOnTarget(
324                 (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
325         target.setRotationChange(change.getEndRotation() - change.getStartRotation());
326         target.backgroundColor = change.getBackgroundColor();
327         return target;
328     }
329 
getDividerTarget(TransitionInfo.Change change, SurfaceControl leash)330     private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
331             SurfaceControl leash) {
332         return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
333                 leash, false /* isTranslucent */, null /* clipRect */,
334                 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
335                 new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
336                 change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
337                 null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
338                 TYPE_DOCK_DIVIDER);
339     }
340 
341     /**
342      * Finds the "correct" root idx for a change. The change's end display is prioritized, then
343      * the start display. If there is no display, it will fallback on the 0th root in the
344      * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
345      */
rootIndexFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)346     public static int rootIndexFor(@NonNull TransitionInfo.Change change,
347             @NonNull TransitionInfo info) {
348         int rootIdx = info.findRootIndex(change.getEndDisplayId());
349         if (rootIdx >= 0) return rootIdx;
350         rootIdx = info.findRootIndex(change.getStartDisplayId());
351         if (rootIdx >= 0) return rootIdx;
352         return 0;
353     }
354 
355     /**
356      * Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}.
357      * @see #rootIndexFor(TransitionInfo.Change, TransitionInfo)
358      */
359     @NonNull
getRootFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)360     public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change,
361             @NonNull TransitionInfo info) {
362         return info.getRoot(rootIndexFor(change, info));
363     }
364 }
365