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