1 /*
2  * Copyright (C) 2020 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.server.wm;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
20 import static android.view.WindowManager.TRANSIT_CHANGE;
21 import static android.view.WindowManager.TRANSIT_CLOSE;
22 import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
23 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
24 import static android.view.WindowManager.TRANSIT_NONE;
25 import static android.view.WindowManager.TRANSIT_OPEN;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.app.ActivityManager;
30 import android.app.IApplicationThread;
31 import android.os.IBinder;
32 import android.os.IRemoteCallback;
33 import android.os.RemoteException;
34 import android.os.SystemClock;
35 import android.util.ArrayMap;
36 import android.util.Slog;
37 import android.util.proto.ProtoOutputStream;
38 import android.view.WindowManager;
39 import android.window.ITransitionMetricsReporter;
40 import android.window.ITransitionPlayer;
41 import android.window.RemoteTransition;
42 import android.window.TransitionInfo;
43 import android.window.TransitionRequestInfo;
44 
45 import com.android.internal.protolog.ProtoLogGroup;
46 import com.android.internal.protolog.common.ProtoLog;
47 import com.android.server.LocalServices;
48 import com.android.server.statusbar.StatusBarManagerInternal;
49 
50 import java.util.ArrayList;
51 import java.util.function.LongConsumer;
52 
53 /**
54  * Handles all the aspects of recording and synchronizing transitions.
55  */
56 class TransitionController {
57     private static final String TAG = "TransitionController";
58 
59     /** The same as legacy APP_TRANSITION_TIMEOUT_MS. */
60     private static final int DEFAULT_TIMEOUT_MS = 5000;
61     /** Less duration for CHANGE type because it does not involve app startup. */
62     private static final int CHANGE_TIMEOUT_MS = 2000;
63 
64     // State constants to line-up with legacy app-transition proto expectations.
65     private static final int LEGACY_STATE_IDLE = 0;
66     private static final int LEGACY_STATE_READY = 1;
67     private static final int LEGACY_STATE_RUNNING = 2;
68 
69     private ITransitionPlayer mTransitionPlayer;
70     final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter();
71 
72     private IApplicationThread mTransitionPlayerThread;
73     final ActivityTaskManagerService mAtm;
74     final TaskSnapshotController mTaskSnapshotController;
75 
76     private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners =
77             new ArrayList<>();
78 
79     /**
80      * Currently playing transitions (in the order they were started). When finished, records are
81      * removed from this list.
82      */
83     private final ArrayList<Transition> mPlayingTransitions = new ArrayList<>();
84 
85     final Lock mRunningLock = new Lock();
86 
87     private final IBinder.DeathRecipient mTransitionPlayerDeath;
88 
89     /** The transition currently being constructed (collecting participants). */
90     private Transition mCollectingTransition = null;
91 
92     // TODO(b/188595497): remove when not needed.
93     final StatusBarManagerInternal mStatusBar;
94 
TransitionController(ActivityTaskManagerService atm, TaskSnapshotController taskSnapshotController)95     TransitionController(ActivityTaskManagerService atm,
96             TaskSnapshotController taskSnapshotController) {
97         mAtm = atm;
98         mStatusBar = LocalServices.getService(StatusBarManagerInternal.class);
99         mTaskSnapshotController = taskSnapshotController;
100         mTransitionPlayerDeath = () -> {
101             synchronized (mAtm.mGlobalLock) {
102                 // Clean-up/finish any playing transitions.
103                 for (int i = 0; i < mPlayingTransitions.size(); ++i) {
104                     mPlayingTransitions.get(i).cleanUpOnFailure();
105                 }
106                 mPlayingTransitions.clear();
107                 mTransitionPlayer = null;
108                 mRunningLock.doNotifyLocked();
109             }
110         };
111     }
112 
113     /** @see #createTransition(int, int) */
114     @NonNull
createTransition(int type)115     Transition createTransition(int type) {
116         return createTransition(type, 0 /* flags */);
117     }
118 
119     /**
120      * Creates a transition. It can immediately collect participants.
121      */
122     @NonNull
createTransition(@indowManager.TransitionType int type, @WindowManager.TransitionFlags int flags)123     private Transition createTransition(@WindowManager.TransitionType int type,
124             @WindowManager.TransitionFlags int flags) {
125         if (mTransitionPlayer == null) {
126             throw new IllegalStateException("Shell Transitions not enabled");
127         }
128         if (mCollectingTransition != null) {
129             throw new IllegalStateException("Simultaneous transitions not supported yet.");
130         }
131         // Distinguish change type because the response time is usually expected to be not too long.
132         final long timeoutMs = type == TRANSIT_CHANGE ? CHANGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
133         mCollectingTransition = new Transition(type, flags, timeoutMs, this,
134                 mAtm.mWindowManager.mSyncEngine);
135         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s",
136                 mCollectingTransition);
137         dispatchLegacyAppTransitionPending();
138         return mCollectingTransition;
139     }
140 
registerTransitionPlayer(@ullable ITransitionPlayer player, @Nullable IApplicationThread appThread)141     void registerTransitionPlayer(@Nullable ITransitionPlayer player,
142             @Nullable IApplicationThread appThread) {
143         try {
144             // Note: asBinder() can be null if player is same process (likely in a test).
145             if (mTransitionPlayer != null) {
146                 if (mTransitionPlayer.asBinder() != null) {
147                     mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
148                 }
149                 mTransitionPlayer = null;
150             }
151             if (player.asBinder() != null) {
152                 player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
153             }
154             mTransitionPlayer = player;
155             mTransitionPlayerThread = appThread;
156         } catch (RemoteException e) {
157             throw new RuntimeException("Unable to set transition player");
158         }
159     }
160 
getTransitionPlayer()161     @Nullable ITransitionPlayer getTransitionPlayer() {
162         return mTransitionPlayer;
163     }
164 
isShellTransitionsEnabled()165     boolean isShellTransitionsEnabled() {
166         return mTransitionPlayer != null;
167     }
168 
169     /**
170      * @return {@code true} if transition is actively collecting changes. This is {@code false}
171      * once a transition is playing
172      */
isCollecting()173     boolean isCollecting() {
174         return mCollectingTransition != null;
175     }
176 
177     /**
178      * @return {@code true} if transition is actively collecting changes and `wc` is one of them.
179      *                      This is {@code false} once a transition is playing.
180      */
isCollecting(@onNull WindowContainer wc)181     boolean isCollecting(@NonNull WindowContainer wc) {
182         return mCollectingTransition != null && mCollectingTransition.mParticipants.contains(wc);
183     }
184 
185     /**
186      * @return {@code true} if transition is actively playing. This is not necessarily {@code true}
187      * during collection.
188      */
isPlaying()189     boolean isPlaying() {
190         return !mPlayingTransitions.isEmpty();
191     }
192 
193     /** @return {@code true} if a transition is running */
inTransition()194     boolean inTransition() {
195         // TODO(shell-transitions): eventually properly support multiple
196         return isCollecting() || isPlaying();
197     }
198 
199     /** @return {@code true} if wc is in a participant subtree */
inTransition(@onNull WindowContainer wc)200     boolean inTransition(@NonNull WindowContainer wc) {
201         if (isCollecting(wc))  return true;
202         for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
203             for (WindowContainer p = wc; p != null; p = p.getParent()) {
204                 if (mPlayingTransitions.get(i).mParticipants.contains(p)) {
205                     return true;
206                 }
207             }
208         }
209         return false;
210     }
211 
212     /**
213      * @return {@code true} if {@param ar} is part of a transient-launch activity in an active
214      * transition.
215      */
isTransientLaunch(@onNull ActivityRecord ar)216     boolean isTransientLaunch(@NonNull ActivityRecord ar) {
217         if (mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar)) {
218             return true;
219         }
220         for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
221             if (mPlayingTransitions.get(i).isTransientLaunch(ar)) return true;
222         }
223         return false;
224     }
225 
226     @WindowManager.TransitionType
getCollectingTransitionType()227     int getCollectingTransitionType() {
228         return mCollectingTransition != null ? mCollectingTransition.mType : TRANSIT_NONE;
229     }
230 
231     /**
232      * @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
233      */
234     @Nullable
requestTransitionIfNeeded(@indowManager.TransitionType int type, @NonNull WindowContainer trigger)235     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
236             @NonNull WindowContainer trigger) {
237         return requestTransitionIfNeeded(type, 0 /* flags */, trigger, trigger /* readyGroupRef */);
238     }
239 
240     /**
241      * @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
242      */
243     @Nullable
requestTransitionIfNeeded(@indowManager.TransitionType int type, @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger, @NonNull WindowContainer readyGroupRef)244     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
245             @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
246             @NonNull WindowContainer readyGroupRef) {
247         return requestTransitionIfNeeded(type, flags, trigger, readyGroupRef,
248                 null /* remoteTransition */);
249     }
250 
isExistenceType(@indowManager.TransitionType int type)251     private static boolean isExistenceType(@WindowManager.TransitionType int type) {
252         return type == TRANSIT_OPEN || type == TRANSIT_CLOSE;
253     }
254 
255     /**
256      * If a transition isn't requested yet, creates one and asks the TransitionPlayer (Shell) to
257      * start it. Collection can start immediately.
258      * @param trigger if non-null, this is the first container that will be collected
259      * @param readyGroupRef Used to identify which ready-group this request is for.
260      * @return the created transition if created or null otherwise.
261      */
262     @Nullable
requestTransitionIfNeeded(@indowManager.TransitionType int type, @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger, @NonNull WindowContainer readyGroupRef, @Nullable RemoteTransition remoteTransition)263     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
264             @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
265             @NonNull WindowContainer readyGroupRef, @Nullable RemoteTransition remoteTransition) {
266         if (mTransitionPlayer == null) {
267             return null;
268         }
269         Transition newTransition = null;
270         if (isCollecting()) {
271             // Make the collecting transition wait until this request is ready.
272             mCollectingTransition.setReady(readyGroupRef, false);
273             if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
274                 // Add keyguard flag to dismiss keyguard
275                 mCollectingTransition.addFlag(flags);
276             }
277         } else {
278             newTransition = requestStartTransition(createTransition(type, flags),
279                     trigger != null ? trigger.asTask() : null, remoteTransition);
280         }
281         if (trigger != null) {
282             if (isExistenceType(type)) {
283                 collectExistenceChange(trigger);
284             } else {
285                 collect(trigger);
286             }
287         }
288         return newTransition;
289     }
290 
291     /** Asks the transition player (shell) to start a created but not yet started transition. */
292     @NonNull
requestStartTransition(@onNull Transition transition, @Nullable Task startTask, @Nullable RemoteTransition remoteTransition)293     Transition requestStartTransition(@NonNull Transition transition, @Nullable Task startTask,
294             @Nullable RemoteTransition remoteTransition) {
295         try {
296             ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
297                     "Requesting StartTransition: %s", transition);
298             ActivityManager.RunningTaskInfo info = null;
299             if (startTask != null) {
300                 info = new ActivityManager.RunningTaskInfo();
301                 startTask.fillTaskInfo(info);
302             }
303             mTransitionPlayer.requestStartTransition(transition, new TransitionRequestInfo(
304                     transition.mType, info, remoteTransition));
305         } catch (RemoteException e) {
306             Slog.e(TAG, "Error requesting transition", e);
307             transition.start();
308         }
309         return transition;
310     }
311 
312     /** Requests transition for a window container which will be removed or invisible. */
requestCloseTransitionIfNeeded(@onNull WindowContainer<?> wc)313     void requestCloseTransitionIfNeeded(@NonNull WindowContainer<?> wc) {
314         if (mTransitionPlayer == null) return;
315         if (wc.isVisibleRequested()) {
316             if (!isCollecting()) {
317                 requestStartTransition(createTransition(TRANSIT_CLOSE, 0 /* flags */),
318                         wc.asTask(), null /* remoteTransition */);
319             }
320             collectExistenceChange(wc);
321         } else {
322             // Removing a non-visible window doesn't require a transition, but if there is one
323             // collecting, this should be a member just in case.
324             collect(wc);
325         }
326     }
327 
328     /** @see Transition#collect */
collect(@onNull WindowContainer wc)329     void collect(@NonNull WindowContainer wc) {
330         if (mCollectingTransition == null) return;
331         mCollectingTransition.collect(wc);
332     }
333 
334     /** @see Transition#collectExistenceChange  */
collectExistenceChange(@onNull WindowContainer wc)335     void collectExistenceChange(@NonNull WindowContainer wc) {
336         if (mCollectingTransition == null) return;
337         mCollectingTransition.collectExistenceChange(wc);
338     }
339 
340     /** @see Transition#setOverrideAnimation */
setOverrideAnimation(TransitionInfo.AnimationOptions options, @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback)341     void setOverrideAnimation(TransitionInfo.AnimationOptions options,
342             @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
343         if (mCollectingTransition == null) return;
344         mCollectingTransition.setOverrideAnimation(options, startCallback, finishCallback);
345     }
346 
347     /** @see Transition#setReady */
setReady(WindowContainer wc, boolean ready)348     void setReady(WindowContainer wc, boolean ready) {
349         if (mCollectingTransition == null) return;
350         mCollectingTransition.setReady(wc, ready);
351     }
352 
353     /** @see Transition#setReady */
setReady(WindowContainer wc)354     void setReady(WindowContainer wc) {
355         setReady(wc, true);
356     }
357 
358     /** @see Transition#finishTransition */
finishTransition(@onNull IBinder token)359     void finishTransition(@NonNull IBinder token) {
360         // It is usually a no-op but make sure that the metric consumer is removed.
361         mTransitionMetricsReporter.reportAnimationStart(token, 0 /* startTime */);
362         final Transition record = Transition.fromBinder(token);
363         if (record == null || !mPlayingTransitions.contains(record)) {
364             Slog.e(TAG, "Trying to finish a non-playing transition " + token);
365             return;
366         }
367         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record);
368         mPlayingTransitions.remove(record);
369         if (mPlayingTransitions.isEmpty()) {
370             setAnimationRunning(false /* running */);
371         }
372         record.finishTransition();
373         mRunningLock.doNotifyLocked();
374     }
375 
moveToPlaying(Transition transition)376     void moveToPlaying(Transition transition) {
377         if (transition != mCollectingTransition) {
378             throw new IllegalStateException("Trying to move non-collecting transition to playing");
379         }
380         mCollectingTransition = null;
381         if (mPlayingTransitions.isEmpty()) {
382             setAnimationRunning(true /* running */);
383         }
384         mPlayingTransitions.add(transition);
385     }
386 
setAnimationRunning(boolean running)387     private void setAnimationRunning(boolean running) {
388         if (mTransitionPlayerThread == null) return;
389         final WindowProcessController wpc = mAtm.getProcessController(mTransitionPlayerThread);
390         if (wpc == null) {
391             Slog.w(TAG, "Unable to find process for player thread=" + mTransitionPlayerThread);
392             return;
393         }
394         wpc.setRunningRemoteAnimation(running);
395     }
396 
abort(Transition transition)397     void abort(Transition transition) {
398         if (transition != mCollectingTransition) {
399             throw new IllegalStateException("Too late to abort.");
400         }
401         transition.abort();
402         mCollectingTransition = null;
403     }
404 
405     /**
406      * Record that the launch of {@param activity} is transient (meaning its lifecycle is currently
407      * tied to the transition).
408      */
setTransientLaunch(@onNull ActivityRecord activity)409     void setTransientLaunch(@NonNull ActivityRecord activity) {
410         if (mCollectingTransition == null) return;
411         mCollectingTransition.setTransientLaunch(activity);
412 
413         // TODO(b/188669821): Remove once legacy recents behavior is moved to shell.
414         // Also interpret HOME transient launch as recents
415         if (activity.getActivityType() == ACTIVITY_TYPE_HOME) {
416             mCollectingTransition.addFlag(TRANSIT_FLAG_IS_RECENTS);
417         }
418     }
419 
legacyDetachNavigationBarFromApp(@onNull IBinder token)420     void legacyDetachNavigationBarFromApp(@NonNull IBinder token) {
421         final Transition transition = Transition.fromBinder(token);
422         if (transition == null || !mPlayingTransitions.contains(transition)) {
423             Slog.e(TAG, "Transition isn't playing: " + token);
424             return;
425         }
426         transition.legacyRestoreNavigationBarFromApp();
427     }
428 
registerLegacyListener(WindowManagerInternal.AppTransitionListener listener)429     void registerLegacyListener(WindowManagerInternal.AppTransitionListener listener) {
430         mLegacyListeners.add(listener);
431     }
432 
unregisterLegacyListener(WindowManagerInternal.AppTransitionListener listener)433     void unregisterLegacyListener(WindowManagerInternal.AppTransitionListener listener) {
434         mLegacyListeners.remove(listener);
435     }
436 
dispatchLegacyAppTransitionPending()437     void dispatchLegacyAppTransitionPending() {
438         for (int i = 0; i < mLegacyListeners.size(); ++i) {
439             mLegacyListeners.get(i).onAppTransitionPendingLocked();
440         }
441     }
442 
dispatchLegacyAppTransitionStarting(TransitionInfo info)443     void dispatchLegacyAppTransitionStarting(TransitionInfo info) {
444         final boolean keyguardGoingAway = info.isKeyguardGoingAway();
445         for (int i = 0; i < mLegacyListeners.size(); ++i) {
446             // TODO(shell-transitions): handle (un)occlude transition.
447             mLegacyListeners.get(i).onAppTransitionStartingLocked(keyguardGoingAway,
448                     false /* keyguardOcclude */, 0 /* durationHint */,
449                     SystemClock.uptimeMillis(), AnimationAdapter.STATUS_BAR_TRANSITION_DURATION);
450         }
451     }
452 
dispatchLegacyAppTransitionFinished(ActivityRecord ar)453     void dispatchLegacyAppTransitionFinished(ActivityRecord ar) {
454         for (int i = 0; i < mLegacyListeners.size(); ++i) {
455             mLegacyListeners.get(i).onAppTransitionFinishedLocked(ar.token);
456         }
457     }
458 
dispatchLegacyAppTransitionCancelled()459     void dispatchLegacyAppTransitionCancelled() {
460         for (int i = 0; i < mLegacyListeners.size(); ++i) {
461             mLegacyListeners.get(i).onAppTransitionCancelledLocked(
462                     false /* keyguardGoingAway */);
463         }
464     }
465 
dumpDebugLegacy(ProtoOutputStream proto, long fieldId)466     void dumpDebugLegacy(ProtoOutputStream proto, long fieldId) {
467         final long token = proto.start(fieldId);
468         int state = LEGACY_STATE_IDLE;
469         if (!mPlayingTransitions.isEmpty()) {
470             state = LEGACY_STATE_RUNNING;
471         } else if (mCollectingTransition != null && mCollectingTransition.getLegacyIsReady()) {
472             state = LEGACY_STATE_READY;
473         }
474         proto.write(AppTransitionProto.APP_TRANSITION_STATE, state);
475         proto.end(token);
476     }
477 
478     static class TransitionMetricsReporter extends ITransitionMetricsReporter.Stub {
479         private final ArrayMap<IBinder, LongConsumer> mMetricConsumers = new ArrayMap<>();
480 
associate(IBinder transitionToken, LongConsumer consumer)481         void associate(IBinder transitionToken, LongConsumer consumer) {
482             synchronized (mMetricConsumers) {
483                 mMetricConsumers.put(transitionToken, consumer);
484             }
485         }
486 
487         @Override
reportAnimationStart(IBinder transitionToken, long startTime)488         public void reportAnimationStart(IBinder transitionToken, long startTime) {
489             final LongConsumer c;
490             synchronized (mMetricConsumers) {
491                 if (mMetricConsumers.isEmpty()) return;
492                 c = mMetricConsumers.remove(transitionToken);
493             }
494             if (c != null) {
495                 c.accept(startTime);
496             }
497         }
498     }
499 
500     class Lock {
501         private int mTransitionWaiters = 0;
runWhenIdle(long timeout, Runnable r)502         void runWhenIdle(long timeout, Runnable r) {
503             synchronized (mAtm.mGlobalLock) {
504                 if (!inTransition()) {
505                     r.run();
506                     return;
507                 }
508                 mTransitionWaiters += 1;
509             }
510             final long startTime = SystemClock.uptimeMillis();
511             final long endTime = startTime + timeout;
512             while (true) {
513                 synchronized (mAtm.mGlobalLock) {
514                     if (!inTransition() || SystemClock.uptimeMillis() > endTime) {
515                         mTransitionWaiters -= 1;
516                         r.run();
517                         return;
518                     }
519                 }
520                 synchronized (this) {
521                     try {
522                         this.wait(timeout);
523                     } catch (InterruptedException e) {
524                         return;
525                     }
526                 }
527             }
528         }
529 
doNotifyLocked()530         void doNotifyLocked() {
531             synchronized (this) {
532                 if (mTransitionWaiters > 0) {
533                     this.notifyAll();
534                 }
535             }
536         }
537     }
538 }
539