• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.LAST_SYSTEM_WINDOW;
25 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
26 import static android.view.WindowManager.TRANSIT_CHANGE;
27 import static android.view.WindowManager.TRANSIT_CLOSE;
28 import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
29 import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
30 import static android.view.WindowManager.TRANSIT_OPEN;
31 import static android.view.WindowManager.TRANSIT_TO_BACK;
32 import static android.view.WindowManager.TRANSIT_TO_FRONT;
33 import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
34 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
35 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
36 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
37 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
38 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
39 
40 import android.annotation.NonNull;
41 import android.annotation.Nullable;
42 import android.annotation.SuppressLint;
43 import android.app.ActivityManager;
44 import android.app.WindowConfiguration;
45 import android.graphics.Rect;
46 import android.util.ArrayMap;
47 import android.util.SparseBooleanArray;
48 import android.view.RemoteAnimationTarget;
49 import android.view.SurfaceControl;
50 import android.view.WindowManager;
51 import android.window.TransitionInfo;
52 
53 import java.util.function.Predicate;
54 
55 /** Various utility functions for transitions. */
56 public class TransitionUtil {
57     /** Flag applied to a transition change to identify it as a divider bar for animation. */
58     public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
59     public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1;
60 
61     /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */
62     public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2;
63 
64     /**
65      * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher.
66      */
67     public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1;
68 
69     /** @return true if the transition was triggered by opening something vs closing something */
isOpeningType(@indowManager.TransitionType int type)70     public static boolean isOpeningType(@WindowManager.TransitionType int type) {
71         return type == TRANSIT_OPEN
72                 || type == TRANSIT_TO_FRONT
73                 || type == TRANSIT_KEYGUARD_GOING_AWAY
74                 || type == TRANSIT_PREPARE_BACK_NAVIGATION;
75     }
76 
77     /** @return true if the transition was triggered by closing something vs opening something */
isClosingType(@indowManager.TransitionType int type)78     public static boolean isClosingType(@WindowManager.TransitionType int type) {
79         return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
80     }
81 
82     /** Returns {@code true} if the transition is opening or closing mode. */
isOpenOrCloseMode(@ransitionInfo.TransitionMode int mode)83     public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
84         return isOpeningMode(mode) || isClosingMode(mode);
85     }
86 
87     /** Returns {@code true} if the transition is opening mode. */
isOpeningMode(@ransitionInfo.TransitionMode int mode)88     public static boolean isOpeningMode(@TransitionInfo.TransitionMode int mode) {
89         return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
90     }
91 
92     /** Returns {@code true} if the transition is closing mode. */
isClosingMode(@ransitionInfo.TransitionMode int mode)93     public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) {
94         return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
95     }
96 
97     /** Returns {@code true} if the transition has a display change. */
hasDisplayChange(@onNull TransitionInfo info)98     public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
99         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
100             final TransitionInfo.Change change = info.getChanges().get(i);
101             if (change.getMode() == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
102                 return true;
103             }
104         }
105         return false;
106     }
107 
108     /** Returns `true` if `change` is a wallpaper. */
isWallpaper(TransitionInfo.Change change)109     public static boolean isWallpaper(TransitionInfo.Change change) {
110         return (change.getTaskInfo() == null)
111                 && change.hasFlags(FLAG_IS_WALLPAPER)
112                 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
113     }
114 
115     /** Returns `true` if `change` is not an app window or wallpaper. */
isNonApp(TransitionInfo.Change change)116     public static boolean isNonApp(TransitionInfo.Change change) {
117         return (change.getTaskInfo() == null)
118                 && !change.hasFlags(FLAG_IS_WALLPAPER)
119                 && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY);
120     }
121 
122     /** Returns `true` if `change` is the divider. */
isDividerBar(TransitionInfo.Change change)123     public static boolean isDividerBar(TransitionInfo.Change change) {
124         return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR);
125     }
126 
127     /** Returns `true` if `change` is an app's dim layer. */
isDimLayer(TransitionInfo.Change change)128     public static boolean isDimLayer(TransitionInfo.Change change) {
129         return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER);
130     }
131 
132     /** Returns `true` if `change` is only re-ordering. */
isOrderOnly(TransitionInfo.Change change)133     public static boolean isOrderOnly(TransitionInfo.Change change) {
134         return change.getMode() == TRANSIT_CHANGE
135                 && (change.getFlags() & FLAG_MOVED_TO_TOP) != 0
136                 && change.getStartAbsBounds().equals(change.getEndAbsBounds())
137                 && (change.getLastParent() == null
138                         || change.getLastParent().equals(change.getParent()));
139     }
140 
141     /**
142      * Check if all changes in this transition are only ordering changes. If so, we won't animate.
143      */
isAllOrderOnly(TransitionInfo info)144     public static boolean isAllOrderOnly(TransitionInfo info) {
145         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
146             if (!isOrderOnly(info.getChanges().get(i))) return false;
147         }
148         return true;
149     }
150 
151     /**
152      * Look through a transition and see if all non-closing changes are no-animation. If so, no
153      * animation should play.
154      */
isAllNoAnimation(TransitionInfo info)155     public static boolean isAllNoAnimation(TransitionInfo info) {
156         if (isClosingType(info.getType())) {
157             // no-animation is only relevant for launching (open) activities.
158             return false;
159         }
160         boolean hasNoAnimation = false;
161         final int changeSize = info.getChanges().size();
162         for (int i = changeSize - 1; i >= 0; --i) {
163             final TransitionInfo.Change change = info.getChanges().get(i);
164             if (isClosingType(change.getMode())) {
165                 // ignore closing apps since they are a side-effect of the transition and don't
166                 // animate.
167                 continue;
168             }
169             if (change.hasFlags(TransitionInfo.FLAG_NO_ANIMATION)) {
170                 hasNoAnimation = true;
171             } else if (!isOrderOnly(change) && !change.hasFlags(TransitionInfo.FLAG_IS_OCCLUDED)) {
172                 // Ignore the order only or occluded changes since they shouldn't be visible during
173                 // animation. For anything else, we need to animate if at-least one relevant
174                 // participant *is* animated,
175                 return false;
176             }
177         }
178         return hasNoAnimation;
179     }
180 
181     /**
182      * Filter that selects leaf-tasks only. THIS IS ORDER-DEPENDENT! For it to work properly, you
183      * MUST call `test` in the same order that the changes appear in the TransitionInfo.
184      */
185     public static class LeafTaskFilter implements Predicate<TransitionInfo.Change> {
186         private final SparseBooleanArray mChildTaskTargets = new SparseBooleanArray();
187 
188         @Override
test(TransitionInfo.Change change)189         public boolean test(TransitionInfo.Change change) {
190             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
191             if (taskInfo == null) return false;
192             // Children always come before parent since changes are in top-to-bottom z-order.
193             boolean hasChildren = mChildTaskTargets.get(taskInfo.taskId);
194             if (taskInfo.hasParentTask()) {
195                 mChildTaskTargets.put(taskInfo.parentTaskId, true);
196             }
197             // If it has children, it's not a leaf.
198             return !hasChildren;
199         }
200     }
201 
202 
newModeToLegacyMode(int newMode)203     private static int newModeToLegacyMode(int newMode) {
204         switch (newMode) {
205             case WindowManager.TRANSIT_OPEN:
206             case WindowManager.TRANSIT_TO_FRONT:
207                 return MODE_OPENING;
208             case WindowManager.TRANSIT_CLOSE:
209             case WindowManager.TRANSIT_TO_BACK:
210                 return MODE_CLOSING;
211             default:
212                 return MODE_CHANGING;
213         }
214     }
215 
216     /**
217      * Very similar to Transitions#setupAnimHierarchy but specialized for leashes.
218      */
219     @SuppressLint("NewApi")
setupLeash(@onNull SurfaceControl leash, @NonNull TransitionInfo.Change change, int layer, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t)220     private static void setupLeash(@NonNull SurfaceControl leash,
221             @NonNull TransitionInfo.Change change, int layer,
222             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
223         final boolean isOpening = TransitionUtil.isOpeningType(info.getType());
224         // Put animating stuff above this line and put static stuff below it.
225         int zSplitLine = info.getChanges().size();
226         // changes should be ordered top-to-bottom in z
227         final int mode = change.getMode();
228 
229         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
230         t.reparent(leash, info.getRoot(rootIdx).getLeash());
231         final Rect absBounds =
232                 (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
233         t.setPosition(leash, absBounds.left - info.getRoot(rootIdx).getOffset().x,
234                 absBounds.top - info.getRoot(rootIdx).getOffset().y);
235 
236         if (isDividerBar(change)) {
237             if (isOpeningType(mode)) {
238                 t.setAlpha(leash, 0.f);
239             }
240             // Set the transition leash position to 0 in case the divider leash position being
241             // taking down.
242             t.setPosition(leash, 0, 0);
243             t.setLayer(leash, Integer.MAX_VALUE);
244             return;
245         }
246         if (isDimLayer(change)) {
247             // When a dim layer gets reparented onto the transition root, we need to zero out its
248             // position so that it's in line with everything else on the transition root. Also,
249             // we need to set a crop because we don't want it applying MATCH_PARENT on the whole
250             // root surface.
251             t.setPosition(leash, 0, 0);
252             t.setCrop(leash, change.getEndAbsBounds());
253         }
254 
255         // Put all the OPEN/SHOW on top
256         if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
257             // Wallpaper is always at the bottom, opening wallpaper on top of closing one.
258             if (mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT) {
259                 t.setLayer(leash, -zSplitLine + info.getChanges().size() - layer);
260             } else {
261                 t.setLayer(leash, -zSplitLine - layer);
262             }
263         } else if (TransitionUtil.isOpeningType(mode)) {
264             if (isOpening) {
265                 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
266                 if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
267                     // if transferred, it should be left visible.
268                     t.setAlpha(leash, 0.f);
269                 }
270             } else {
271                 // put on bottom and leave it visible
272                 t.setLayer(leash, zSplitLine - layer);
273             }
274         } else if (TransitionUtil.isClosingType(mode)) {
275             if (isOpening) {
276                 // put on bottom and leave visible
277                 t.setLayer(leash, zSplitLine - layer);
278             } else {
279                 // put on top
280                 t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
281             }
282         } else { // CHANGE
283             t.setLayer(leash, zSplitLine + info.getChanges().size() - layer);
284         }
285     }
286 
287     @SuppressLint("NewApi")
createLeash(TransitionInfo info, TransitionInfo.Change change, int order, SurfaceControl.Transaction t)288     private static SurfaceControl createLeash(TransitionInfo info, TransitionInfo.Change change,
289             int order, SurfaceControl.Transaction t) {
290         // TODO: once we can properly sync transactions across process, then get rid of this leash.
291         if (change.getParent() != null && (change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
292             // Special case for wallpaper atm. Normally these are left alone; but, a quirk of
293             // making leashes means we have to handle them specially.
294             return change.getLeash();
295         }
296         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
297         SurfaceControl leashSurface = new SurfaceControl.Builder()
298                 .setName(change.getLeash().toString() + "_transition-leash")
299                 .setContainerLayer()
300                 // Initial the surface visible to respect the visibility of the original surface.
301                 .setHidden(false)
302                 .setParent(info.getRoot(rootIdx).getLeash())
303                 .build();
304         // Copied Transitions setup code (which expects bottom-to-top order, so we swap here)
305         setupLeash(leashSurface, change, info.getChanges().size() - order, info, t);
306         t.reparent(change.getLeash(), leashSurface);
307         if (!isDimLayer(change)) {
308             // Most leashes going onto the transition root should have their alpha set here to make
309             // them visible. But dim layers should be left untouched (their alpha value is their
310             // actual dim value).
311             t.setAlpha(change.getLeash(), 1.0f);
312         }
313         if (!isDividerBar(change)) {
314             // For divider, don't modify its inner leash position when creating the outer leash
315             // for the transition. In case the position being wrong after the transition finished.
316             t.setPosition(change.getLeash(), 0, 0);
317         }
318         t.setLayer(change.getLeash(), 0);
319         t.show(change.getLeash());
320         return leashSurface;
321     }
322 
323     /**
324      * Creates a new RemoteAnimationTarget from the provided change info
325      */
newTarget(TransitionInfo.Change change, int order, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)326     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
327             TransitionInfo info, SurfaceControl.Transaction t,
328             @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
329         return newTarget(change, order, false /* forceTranslucent */, info, t, leashMap);
330     }
331 
332     /**
333      * Creates a new RemoteAnimationTarget from the provided change info
334      */
newTarget(TransitionInfo.Change change, int order, boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t, @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap)335     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
336             boolean forceTranslucent, TransitionInfo info, SurfaceControl.Transaction t,
337             @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
338         final SurfaceControl leash = createLeash(info, change, order, t);
339         if (leashMap != null) {
340             leashMap.put(change.getLeash(), leash);
341         }
342         return newTarget(change, order, leash, forceTranslucent);
343     }
344 
345     /**
346      * Creates a new RemoteAnimationTarget from the provided change and leash
347      */
newTarget(TransitionInfo.Change change, int order, SurfaceControl leash)348     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
349             SurfaceControl leash) {
350         return newTarget(change, order, leash, false /* forceTranslucent */);
351     }
352 
353     /**
354      * Creates a new RemoteAnimationTarget from the provided change and leash
355      */
newTarget(TransitionInfo.Change change, int order, SurfaceControl leash, boolean forceTranslucent)356     public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
357             SurfaceControl leash, boolean forceTranslucent) {
358         if (isDividerBar(change)) {
359             return getDividerTarget(change, leash);
360         }
361         if (isDimLayer(change)) {
362             return getDimLayerTarget(change, leash);
363         }
364 
365         int taskId;
366         boolean isNotInRecents;
367         ActivityManager.RunningTaskInfo taskInfo;
368         WindowConfiguration windowConfiguration;
369 
370         taskInfo = change.getTaskInfo();
371         if (taskInfo != null) {
372             taskId = taskInfo.taskId;
373             isNotInRecents = !taskInfo.isRunning;
374             windowConfiguration = taskInfo.configuration.windowConfiguration;
375         } else {
376             taskId = INVALID_TASK_ID;
377             isNotInRecents = true;
378             windowConfiguration = new WindowConfiguration();
379         }
380 
381         Rect localBounds = new Rect(change.getEndAbsBounds());
382         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
383 
384         RemoteAnimationTarget target = new RemoteAnimationTarget(
385                 taskId,
386                 newModeToLegacyMode(change.getMode()),
387                 // TODO: once we can properly sync transactions across process,
388                 // then get rid of this leash.
389                 leash,
390                 forceTranslucent || (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
391                 null,
392                 // TODO(shell-transitions): we need to send content insets? evaluate how its used.
393                 new Rect(0, 0, 0, 0),
394                 order,
395                 null,
396                 localBounds,
397                 new Rect(change.getEndAbsBounds()),
398                 windowConfiguration,
399                 isNotInRecents,
400                 null,
401                 new Rect(change.getStartAbsBounds()),
402                 taskInfo,
403                 change.isAllowEnterPip(),
404                 INVALID_WINDOW_TYPE
405         );
406         target.setWillShowImeOnTarget(
407                 (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
408         target.setRotationChange(change.getEndRotation() - change.getStartRotation());
409         target.backgroundColor = change.getBackgroundColor();
410         return target;
411     }
412 
413     /**
414      * Creates a new RemoteAnimationTarget from the provided change and leash
415      */
newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order, boolean isTranslucent)416     public static RemoteAnimationTarget newSyntheticTarget(ActivityManager.RunningTaskInfo taskInfo,
417             SurfaceControl leash, @TransitionInfo.TransitionMode int mode, int order,
418             boolean isTranslucent) {
419         int taskId;
420         boolean isNotInRecents;
421         WindowConfiguration windowConfiguration;
422 
423         if (taskInfo != null) {
424             taskId = taskInfo.taskId;
425             isNotInRecents = !taskInfo.isRunning;
426             windowConfiguration = taskInfo.configuration.windowConfiguration;
427         } else {
428             taskId = INVALID_TASK_ID;
429             isNotInRecents = true;
430             windowConfiguration = new WindowConfiguration();
431         }
432 
433         Rect bounds = windowConfiguration.getBounds();
434         RemoteAnimationTarget target = new RemoteAnimationTarget(
435                 taskId,
436                 newModeToLegacyMode(mode),
437                 // TODO: once we can properly sync transactions across process,
438                 // then get rid of this leash.
439                 leash,
440                 isTranslucent,
441                 null,
442                 // TODO(shell-transitions): we need to send content insets? evaluate how its used.
443                 new Rect(0, 0, 0, 0),
444                 order,
445                 null,
446                 bounds,
447                 bounds,
448                 windowConfiguration,
449                 isNotInRecents,
450                 null,
451                 bounds,
452                 taskInfo,
453                 false,
454                 INVALID_WINDOW_TYPE
455         );
456         return target;
457     }
458 
getDividerTarget(TransitionInfo.Change change, SurfaceControl leash)459     private static RemoteAnimationTarget getDividerTarget(TransitionInfo.Change change,
460             SurfaceControl leash) {
461         return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
462                 leash, false /* isTranslucent */, null /* clipRect */,
463                 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
464                 new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
465                 change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
466                 null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
467                 TYPE_DOCK_DIVIDER);
468     }
469 
getDimLayerTarget(TransitionInfo.Change change, SurfaceControl leash)470     private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change,
471             SurfaceControl leash) {
472         return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()),
473                 leash, false /* isTranslucent */, null /* clipRect */,
474                 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
475                 new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(),
476                 change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */,
477                 null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */,
478                 TYPE_SPLIT_SCREEN_DIM_LAYER);
479     }
480 
481     /**
482      * Finds the "correct" root idx for a change. The change's end display is prioritized, then
483      * the start display. If there is no display, it will fallback on the 0th root in the
484      * transition. There MUST be at-least 1 root in the transition (ie. it's not a no-op).
485      */
rootIndexFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)486     public static int rootIndexFor(@NonNull TransitionInfo.Change change,
487             @NonNull TransitionInfo info) {
488         int rootIdx = info.findRootIndex(change.getEndDisplayId());
489         if (rootIdx >= 0) return rootIdx;
490         rootIdx = info.findRootIndex(change.getStartDisplayId());
491         if (rootIdx >= 0) return rootIdx;
492         return 0;
493     }
494 
495     /**
496      * Gets the {@link TransitionInfo.Root} for the given {@link TransitionInfo.Change}.
497      * @see #rootIndexFor(TransitionInfo.Change, TransitionInfo)
498      */
499     @NonNull
getRootFor(@onNull TransitionInfo.Change change, @NonNull TransitionInfo info)500     public static TransitionInfo.Root getRootFor(@NonNull TransitionInfo.Change change,
501             @NonNull TransitionInfo info) {
502         return info.getRoot(rootIndexFor(change, info));
503     }
504 }
505