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