• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.systemui.animation;
18 
19 import static android.view.WindowManager.TRANSIT_CHANGE;
20 
21 import android.animation.Animator;
22 import android.animation.Animator.AnimatorListener;
23 import android.animation.ValueAnimator;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.graphics.Rect;
27 import android.hardware.display.DisplayManager;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.DisplayMetrics;
32 import android.util.Log;
33 import android.view.SurfaceControl;
34 import android.window.IRemoteTransition;
35 import android.window.IRemoteTransitionFinishedCallback;
36 import android.window.TransitionInfo;
37 import android.window.TransitionInfo.Change;
38 import android.window.WindowAnimationState;
39 
40 import com.android.internal.policy.ScreenDecorationsUtils;
41 import com.android.wm.shell.shared.TransitionUtil;
42 
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.concurrent.atomic.AtomicBoolean;
47 
48 /**
49  * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
50  * and automatically attaches it to the transition leash before the transition starts.
51  *
52  * @hide
53  */
54 public class OriginRemoteTransition extends IRemoteTransition.Stub {
55     private static final String TAG = "OriginRemoteTransition";
56     private static final long FINISH_ANIMATION_TIMEOUT_MS = 100;
57 
58     private final Context mContext;
59     private final boolean mIsEntry;
60     private final UIComponent mOrigin;
61     private final TransitionPlayer mPlayer;
62     private final long mDuration;
63     private final Handler mHandler;
64 
65     @Nullable private SurfaceControl.Transaction mStartTransaction;
66     @Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
67     @Nullable private UIComponent.Transaction mOriginTransaction;
68     @Nullable private ValueAnimator mAnimator;
69     @Nullable private SurfaceControl mOriginLeash;
70     private boolean mCancelled;
71 
OriginRemoteTransition( Context context, boolean isEntry, UIComponent origin, TransitionPlayer player, long duration, Handler handler)72     OriginRemoteTransition(
73             Context context,
74             boolean isEntry,
75             UIComponent origin,
76             TransitionPlayer player,
77             long duration,
78             Handler handler) {
79         mContext = context;
80         mIsEntry = isEntry;
81         mOrigin = origin;
82         mPlayer = player;
83         mDuration = duration;
84         mHandler = handler;
85     }
86 
87     @Override
startAnimation( IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)88     public void startAnimation(
89             IBinder token,
90             TransitionInfo info,
91             SurfaceControl.Transaction t,
92             IRemoteTransitionFinishedCallback finishCallback) {
93         logD("startAnimation - " + info);
94         mHandler.post(
95                 () -> {
96                     mStartTransaction = t;
97                     mFinishCallback = finishCallback;
98                     startAnimationInternal(info, /* states= */ null);
99                 });
100     }
101 
102     @Override
mergeAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback)103     public void mergeAnimation(
104             IBinder transition,
105             TransitionInfo info,
106             SurfaceControl.Transaction t,
107             IBinder mergeTarget,
108             IRemoteTransitionFinishedCallback finishCallback) {
109         logD("mergeAnimation - " + info);
110         cancel();
111     }
112 
113     @Override
takeOverAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)114     public void takeOverAnimation(
115             IBinder transition,
116             TransitionInfo info,
117             SurfaceControl.Transaction t,
118             IRemoteTransitionFinishedCallback finishCallback,
119             WindowAnimationState[] states) {
120         logD("takeOverAnimation - info=" + info + ", states=" + Arrays.toString(states));
121         mHandler.post(
122                 () -> {
123                     mStartTransaction = t;
124                     mFinishCallback = finishCallback;
125                     startAnimationInternal(info, states);
126                 });
127     }
128 
129     @Override
onTransitionConsumed(IBinder transition, boolean aborted)130     public void onTransitionConsumed(IBinder transition, boolean aborted) {
131         logD("onTransitionConsumed - aborted: " + aborted);
132         cancel();
133     }
134 
startAnimationInternal( TransitionInfo info, @Nullable WindowAnimationState[] states)135     private void startAnimationInternal(
136             TransitionInfo info, @Nullable WindowAnimationState[] states) {
137         if (!prepareUIs(info)) {
138             logE("Unable to prepare UI!");
139             finishAnimation(/* finished= */ false);
140             return;
141         }
142         // Notify player that we are starting.
143         mPlayer.onStart(info, states, mStartTransaction, mOrigin, mOriginTransaction);
144 
145         // Apply the initial transactions in case the player forgot to apply them.
146         mOriginTransaction.commit();
147         mStartTransaction.apply();
148 
149         // Start the animator.
150         mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
151         mAnimator.setDuration(mDuration);
152         mAnimator.addListener(
153                 new AnimatorListener() {
154                     @Override
155                     public void onAnimationStart(Animator a) {}
156 
157                     @Override
158                     public void onAnimationEnd(Animator a) {
159                         finishAnimation(/* finished= */ !mCancelled);
160                     }
161 
162                     @Override
163                     public void onAnimationCancel(Animator a) {
164                         mCancelled = true;
165                     }
166 
167                     @Override
168                     public void onAnimationRepeat(Animator a) {}
169                 });
170         mAnimator.addUpdateListener(
171                 a -> {
172                     mPlayer.onProgress((float) a.getAnimatedValue());
173                 });
174         mAnimator.start();
175     }
176 
prepareUIs(TransitionInfo info)177     private boolean prepareUIs(TransitionInfo info) {
178         if (info.getRootCount() == 0) {
179             logE("prepareUIs: no root leash!");
180             return false;
181         }
182         if (info.getRootCount() > 1) {
183             logE("prepareUIs: multi-display transition is not supported yet!");
184             return false;
185         }
186         if (info.getChanges().isEmpty()) {
187             logE("prepareUIs: no changes!");
188             return false;
189         }
190 
191         SurfaceControl rootLeash = info.getRoot(0).getLeash();
192         int displayId = info.getChanges().get(0).getEndDisplayId();
193         Rect displayBounds = getDisplayBounds(displayId);
194         float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
195         logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);
196 
197         // Create the origin leash and add to the transition root leash.
198         mOriginLeash =
199                 new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
200 
201         // Create temporary transaction to build
202         final SurfaceControl.Transaction tmpTransaction = new SurfaceControl.Transaction();
203         tmpTransaction
204                 .reparent(mOriginLeash, rootLeash)
205                 .show(mOriginLeash)
206                 .setCornerRadius(mOriginLeash, windowRadius)
207                 .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());
208 
209         // Process surfaces
210         List<SurfaceControl> openingSurfaces = new ArrayList<>();
211         List<SurfaceControl> closingSurfaces = new ArrayList<>();
212         for (Change change : info.getChanges()) {
213             int mode = change.getMode();
214             SurfaceControl leash = change.getLeash();
215             // Reparent leash to the transition root.
216             tmpTransaction.reparent(leash, rootLeash);
217             if (TransitionUtil.isOpeningMode(mode)) {
218                 openingSurfaces.add(change.getLeash());
219                 // For opening surfaces, ending bounds are base bound. Apply corner radius if
220                 // it's full screen.
221                 Rect bounds = change.getEndAbsBounds();
222                 if (displayBounds.equals(bounds)) {
223                     tmpTransaction
224                             .setCornerRadius(leash, windowRadius)
225                             .setWindowCrop(leash, bounds.width(), bounds.height());
226                 }
227             } else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) {
228                 // TRANSIT_CHANGE refers to the closing window in predictive back animation.
229                 closingSurfaces.add(change.getLeash());
230                 // For closing surfaces, starting bounds are base bounds. Apply corner radius if
231                 // it's full screen.
232                 Rect bounds = change.getStartAbsBounds();
233                 if (displayBounds.equals(bounds)) {
234                     tmpTransaction
235                             .setCornerRadius(leash, windowRadius)
236                             .setWindowCrop(leash, bounds.width(), bounds.height());
237                 }
238             }
239         }
240 
241         if (openingSurfaces.isEmpty() && closingSurfaces.isEmpty()) {
242             logD("prepareUIs: no opening/closing surfaces available, nothing to prepare.");
243             return false;
244         }
245 
246         // Set relative order:
247         // ----  App1  ----
248         // ---- origin ----
249         // ----  App2  ----
250 
251         if (mIsEntry) {
252             if (!closingSurfaces.isEmpty()) {
253                 tmpTransaction.setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1);
254             } else {
255                 logW("Missing closing surface is entry transition");
256             }
257             if (!openingSurfaces.isEmpty()) {
258                 tmpTransaction.setRelativeLayer(
259                         openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
260             } else {
261                 logW("Missing opening surface is entry transition");
262             }
263 
264         } else {
265             if (!openingSurfaces.isEmpty()) {
266                 tmpTransaction.setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1);
267             } else {
268                 logW("Missing opening surface is exit transition");
269             }
270             if (!closingSurfaces.isEmpty()) {
271                 tmpTransaction.setRelativeLayer(
272                         closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
273             } else {
274                 logW("Missing closing surface is exit transition");
275             }
276         }
277         mStartTransaction.merge(tmpTransaction);
278 
279         // Attach origin UIComponent to origin leash.
280         mOriginTransaction = mOrigin.newTransaction();
281         mOriginTransaction.attachToTransitionLeash(
282                 mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height());
283         return true;
284     }
285 
getDisplayBounds(int displayId)286     private Rect getDisplayBounds(int displayId) {
287         DisplayManager dm = mContext.getSystemService(DisplayManager.class);
288         DisplayMetrics metrics = new DisplayMetrics();
289         dm.getDisplay(displayId).getMetrics(metrics);
290         return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
291     }
292 
finishAnimation(boolean finished)293     private void finishAnimation(boolean finished) {
294         logD("finishAnimation: finished=" + finished);
295         OneShotRunnable finishInternalRunnable = new OneShotRunnable(this::finishInternal);
296         Runnable timeoutRunnable =
297                 () -> {
298                     Log.w(TAG, "Timeout waiting for surface transaction!");
299                     finishInternalRunnable.run();
300                 };
301         Runnable committedRunnable =
302                 () -> {
303                     // Remove the timeout runnable.
304                     mHandler.removeCallbacks(timeoutRunnable);
305                     finishInternalRunnable.run();
306                 };
307         if (mAnimator == null) {
308             // The transition didn't start. Ensure we apply the start transaction and report
309             // finish afterwards.
310             mStartTransaction
311                     .addTransactionCommittedListener(mHandler::post, committedRunnable::run)
312                     .apply();
313             // Call finishInternal() anyway after the timeout.
314             mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS);
315             return;
316         }
317         mAnimator = null;
318         // Notify client that we have ended.
319         mPlayer.onEnd(finished);
320         // Detach the origin from the transition leash and report finish after it's done.
321         mOriginTransaction
322                 .detachFromTransitionLeash(mOrigin, mHandler::post, committedRunnable)
323                 .commit();
324         // Call finishInternal() anyway after the timeout.
325         mHandler.postDelayed(timeoutRunnable, FINISH_ANIMATION_TIMEOUT_MS);
326     }
327 
finishInternal()328     private void finishInternal() {
329         logD("finishInternal");
330         if (mOriginLeash != null) {
331             // Release origin leash.
332             mOriginLeash.release();
333             mOriginLeash = null;
334         }
335         try {
336             mFinishCallback.onTransitionFinished(null, null);
337         } catch (RemoteException e) {
338             logE("Unable to report transition finish!", e);
339         }
340         mStartTransaction = null;
341         mOriginTransaction = null;
342         mFinishCallback = null;
343     }
344 
cancel()345     public void cancel() {
346         logD("cancel()");
347         mHandler.post(
348                 () -> {
349                     if (mAnimator != null) {
350                         mAnimator.cancel();
351                     }
352                 });
353     }
354 
logD(String msg)355     private static void logD(String msg) {
356         if (OriginTransitionSession.DEBUG) {
357             Log.d(TAG, msg);
358         }
359     }
360 
logW(String msg)361     private static void logW(String msg) {
362         Log.w(TAG, msg);
363     }
364 
logE(String msg)365     private static void logE(String msg) {
366         Log.e(TAG, msg);
367     }
368 
logE(String msg, Throwable e)369     private static void logE(String msg, Throwable e) {
370         Log.e(TAG, msg, e);
371     }
372 
wrapSurfaces(TransitionInfo info, boolean isOpening)373     private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
374         List<SurfaceControl> surfaces = new ArrayList<>();
375         Rect maxBounds = new Rect();
376         for (Change change : info.getChanges()) {
377             int mode = change.getMode();
378             if (TransitionUtil.isOpeningMode(mode) == isOpening) {
379                 surfaces.add(change.getLeash());
380                 Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
381                 maxBounds.union(bounds);
382             }
383         }
384         return new SurfaceUIComponent(
385                 surfaces,
386                 /* alpha= */ 1.0f,
387                 /* visible= */ true,
388                 /* bounds= */ maxBounds,
389                 /* baseBounds= */ maxBounds);
390     }
391 
applyWindowAnimationStates( TransitionInfo info, @Nullable WindowAnimationState[] states, UIComponent closingApp, UIComponent openingApp)392     private static void applyWindowAnimationStates(
393             TransitionInfo info,
394             @Nullable WindowAnimationState[] states,
395             UIComponent closingApp,
396             UIComponent openingApp) {
397         if (states == null) {
398             // Nothing to apply.
399             return;
400         }
401         // Calculate bounds.
402         Rect maxClosingBounds = new Rect();
403         Rect maxOpeningBounds = new Rect();
404         for (int i = 0; i < info.getChanges().size(); i++) {
405             Rect bound = getBounds(states[i]);
406             if (bound == null) {
407                 continue;
408             }
409             int mode = info.getChanges().get(i).getMode();
410             if (TransitionUtil.isOpeningMode(mode)) {
411                 maxOpeningBounds.union(bound);
412             } else if (TransitionUtil.isClosingMode(mode) || mode == TRANSIT_CHANGE) {
413                 // TRANSIT_CHANGE refers to the closing window in predictive back animation.
414                 maxClosingBounds.union(bound);
415             }
416         }
417 
418         // Intentionally use a new transaction instead of reusing the existing transaction since we
419         // want to apply window animation states first without committing any other pending changes
420         // in the existing transaction. The existing transaction is expected to be committed by the
421         // onStart() client callback together with client's custom transformation.
422         UIComponent.Transaction transaction = closingApp.newTransaction();
423         if (!maxClosingBounds.isEmpty()) {
424             logD("Applying closing window bounds: " + maxClosingBounds);
425             transaction.setBounds(closingApp, maxClosingBounds);
426         }
427         if (!maxOpeningBounds.isEmpty()) {
428             logD("Applying opening window bounds: " + maxOpeningBounds);
429             transaction.setBounds(openingApp, maxOpeningBounds);
430         }
431         transaction.commit();
432     }
433 
434     @Nullable
getBounds(@ullable WindowAnimationState state)435     private static Rect getBounds(@Nullable WindowAnimationState state) {
436         if (state == null || state.bounds == null) {
437             return null;
438         }
439         Rect out = new Rect();
440         state.bounds.roundOut(out);
441         return out;
442     }
443 
444     /** A {@link Runnable} that will only run once. */
445     private static class OneShotRunnable implements Runnable {
446         private final AtomicBoolean mDone = new AtomicBoolean();
447         private final Runnable mRunnable;
448 
OneShotRunnable(Runnable runnable)449         OneShotRunnable(Runnable runnable) {
450             this.mRunnable = runnable;
451         }
452 
453         @Override
run()454         public void run() {
455             if (!mDone.getAndSet(true)) {
456                 mRunnable.run();
457             }
458         }
459     }
460 
461     /**
462      * An interface that represents an origin transitions.
463      *
464      * @hide
465      */
466     public interface TransitionPlayer {
467 
468         /**
469          * Called when an origin transition starts. This method exposes the raw {@link
470          * TransitionInfo} so that clients can extract more information from it.
471          *
472          * <p>Note: if this transition is taking over a predictive back animation, the {@link
473          * WindowAnimationState} will be passed to this method. The concrete implementation is
474          * expected to apply the {@link WindowAnimationState} before continuing the transition.
475          */
onStart( TransitionInfo transitionInfo, @Nullable WindowAnimationState[] states, SurfaceControl.Transaction sfTransaction, UIComponent origin, UIComponent.Transaction uiTransaction)476         default void onStart(
477                 TransitionInfo transitionInfo,
478                 @Nullable WindowAnimationState[] states,
479                 SurfaceControl.Transaction sfTransaction,
480                 UIComponent origin,
481                 UIComponent.Transaction uiTransaction) {
482             // Wrap transactions.
483             Transactions transactions =
484                     new Transactions()
485                             .registerTransactionForClass(origin.getClass(), uiTransaction)
486                             .registerTransactionForClass(
487                                     SurfaceUIComponent.class,
488                                     new SurfaceUIComponent.Transaction(sfTransaction));
489             // Wrap surfaces.
490             UIComponent closingApp = wrapSurfaces(transitionInfo, /* isOpening= */ false);
491             UIComponent openingApp = wrapSurfaces(transitionInfo, /* isOpening= */ true);
492 
493             // Restore the pending animation states coming from predictive back transition.
494             applyWindowAnimationStates(transitionInfo, states, closingApp, openingApp);
495 
496             // Start.
497             onStart(transactions, origin, closingApp, openingApp);
498         }
499 
500         /**
501          * Called when an origin transition starts. This method exposes the opening and closing
502          * windows as wrapped {@link UIComponent} to provide simplified interface to clients.
503          */
onStart( UIComponent.Transaction transaction, UIComponent origin, UIComponent closingApp, UIComponent openingApp)504         void onStart(
505                 UIComponent.Transaction transaction,
506                 UIComponent origin,
507                 UIComponent closingApp,
508                 UIComponent openingApp);
509 
510         /** Called to update the transition frame. */
onProgress(float progress)511         void onProgress(float progress);
512 
513         /** Called when the transition ended. */
onEnd(boolean finished)514         void onEnd(boolean finished);
515     }
516 }
517