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.server; 18 19 import static android.view.WindowManager.TRANSIT_CHANGE; 20 import static android.view.WindowManager.TRANSIT_CLOSE; 21 import static android.view.WindowManager.TRANSIT_OPEN; 22 import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; 23 import static android.view.WindowManager.TRANSIT_TO_BACK; 24 import static android.view.WindowManager.TRANSIT_TO_FRONT; 25 26 import android.Manifest; 27 import android.annotation.Nullable; 28 import android.app.TaskInfo; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.os.Build; 32 import android.os.IBinder; 33 import android.os.RemoteException; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 import android.view.SurfaceControl; 37 import android.window.IRemoteTransition; 38 import android.window.IRemoteTransitionFinishedCallback; 39 import android.window.RemoteTransition; 40 import android.window.TransitionFilter; 41 import android.window.TransitionInfo; 42 import android.window.TransitionInfo.Change; 43 import android.window.WindowAnimationState; 44 45 import com.android.internal.annotations.GuardedBy; 46 import com.android.internal.util.IndentingPrintWriter; 47 import com.android.systemui.animation.shared.IOriginTransitions; 48 import com.android.wm.shell.shared.ShellTransitions; 49 import com.android.wm.shell.shared.TransitionUtil; 50 51 import java.util.Map; 52 import java.util.concurrent.Executor; 53 import java.util.function.Predicate; 54 55 /** An implementation of the {@link IOriginTransitions}. */ 56 public class IOriginTransitionsImpl extends IOriginTransitions.Stub { 57 private static final String TAG = "OriginTransitions"; 58 private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG); 59 60 private final Object mLock = new Object(); 61 private final ShellTransitions mShellTransitions; 62 private final Context mContext; 63 64 @GuardedBy("mLock") 65 private final Map<IBinder, OriginTransitionRecord> mRecords = new ArrayMap<>(); 66 IOriginTransitionsImpl(Context context, ShellTransitions shellTransitions)67 public IOriginTransitionsImpl(Context context, ShellTransitions shellTransitions) { 68 mShellTransitions = shellTransitions; 69 mContext = context; 70 } 71 72 @Override makeOriginTransition( RemoteTransition launchTransition, RemoteTransition returnTransition)73 public RemoteTransition makeOriginTransition( 74 RemoteTransition launchTransition, RemoteTransition returnTransition) 75 throws RemoteException { 76 if (DEBUG) { 77 Log.d( 78 TAG, 79 "makeOriginTransition: (" + launchTransition + ", " + returnTransition + ")"); 80 } 81 enforceRemoteTransitionPermission(); 82 synchronized (mLock) { 83 OriginTransitionRecord record = 84 new OriginTransitionRecord(launchTransition, returnTransition); 85 mRecords.put(record.getToken(), record); 86 return record.asLaunchableTransition(); 87 } 88 } 89 90 @Override cancelOriginTransition(RemoteTransition originTransition)91 public void cancelOriginTransition(RemoteTransition originTransition) { 92 if (DEBUG) { 93 Log.d(TAG, "cancelOriginTransition: " + originTransition); 94 } 95 enforceRemoteTransitionPermission(); 96 synchronized (mLock) { 97 if (!mRecords.containsKey(originTransition.asBinder())) { 98 return; 99 } 100 mRecords.get(originTransition.asBinder()).destroy(); 101 } 102 } 103 enforceRemoteTransitionPermission()104 private void enforceRemoteTransitionPermission() { 105 mContext.enforceCallingPermission( 106 Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, 107 "Missing permission " 108 + Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS); 109 } 110 dump(IndentingPrintWriter ipw)111 public void dump(IndentingPrintWriter ipw) { 112 ipw.println("IOriginTransitionsImpl"); 113 ipw.println("Active records:"); 114 ipw.increaseIndent(); 115 synchronized (mLock) { 116 if (mRecords.isEmpty()) { 117 ipw.println("none"); 118 } else { 119 for (OriginTransitionRecord record : mRecords.values()) { 120 record.dump(ipw); 121 } 122 } 123 } 124 ipw.decreaseIndent(); 125 } 126 127 /** 128 * An {@link IRemoteTransition} that delegates animation to another {@link IRemoteTransition} 129 * and notify callbacks when the transition starts. 130 */ 131 private static class RemoteTransitionDelegate extends IRemoteTransition.Stub { 132 private final IRemoteTransition mTransition; 133 private final Predicate<TransitionInfo> mOnStarting; 134 private final Executor mExecutor; 135 RemoteTransitionDelegate( Executor executor, IRemoteTransition transition, Predicate<TransitionInfo> onStarting)136 RemoteTransitionDelegate( 137 Executor executor, 138 IRemoteTransition transition, 139 Predicate<TransitionInfo> onStarting) { 140 mExecutor = executor; 141 mTransition = transition; 142 mOnStarting = onStarting; 143 } 144 145 @Override startAnimation( IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)146 public void startAnimation( 147 IBinder token, 148 TransitionInfo info, 149 SurfaceControl.Transaction t, 150 IRemoteTransitionFinishedCallback finishCallback) 151 throws RemoteException { 152 if (DEBUG) { 153 Log.d(TAG, "startAnimation: " + info); 154 } 155 if (maybeInterceptTransition(info, t, finishCallback)) { 156 return; 157 } 158 mTransition.startAnimation(token, info, t, finishCallback); 159 } 160 161 @Override mergeAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback)162 public void mergeAnimation( 163 IBinder transition, 164 TransitionInfo info, 165 SurfaceControl.Transaction t, 166 IBinder mergeTarget, 167 IRemoteTransitionFinishedCallback finishCallback) 168 throws RemoteException { 169 if (DEBUG) { 170 Log.d(TAG, "mergeAnimation: " + info); 171 } 172 mTransition.mergeAnimation(transition, info, t, mergeTarget, finishCallback); 173 } 174 175 @Override takeOverAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)176 public void takeOverAnimation( 177 IBinder transition, 178 TransitionInfo info, 179 SurfaceControl.Transaction t, 180 IRemoteTransitionFinishedCallback finishCallback, 181 WindowAnimationState[] states) 182 throws RemoteException { 183 if (DEBUG) { 184 Log.d(TAG, "takeOverAnimation: " + info); 185 } 186 if (maybeInterceptTransition(info, t, finishCallback)) { 187 return; 188 } 189 mTransition.takeOverAnimation(transition, info, t, finishCallback, states); 190 } 191 192 @Override onTransitionConsumed(IBinder transition, boolean aborted)193 public void onTransitionConsumed(IBinder transition, boolean aborted) 194 throws RemoteException { 195 if (DEBUG) { 196 Log.d(TAG, "onTransitionConsumed: aborted=" + aborted); 197 } 198 mTransition.onTransitionConsumed(transition, aborted); 199 } 200 201 @Override toString()202 public String toString() { 203 return "RemoteTransitionDelegate{transition=" + mTransition + "}"; 204 } 205 maybeInterceptTransition( TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)206 private boolean maybeInterceptTransition( 207 TransitionInfo info, 208 SurfaceControl.Transaction t, 209 IRemoteTransitionFinishedCallback finishCallback) { 210 if (!mOnStarting.test(info)) { 211 Log.w(TAG, "Intercepting cancelled transition " + mTransition); 212 t.addTransactionCommittedListener( 213 mExecutor, 214 () -> { 215 try { 216 finishCallback.onTransitionFinished(null, null); 217 } catch (RemoteException e) { 218 Log.e(TAG, "Unable to report finish.", e); 219 } 220 }) 221 .apply(); 222 return true; 223 } 224 return false; 225 } 226 } 227 228 /** A data record containing the origin transition pieces. */ 229 private class OriginTransitionRecord implements IBinder.DeathRecipient { 230 private final RemoteTransition mWrappedLaunchTransition; 231 private final RemoteTransition mWrappedReturnTransition; 232 233 @GuardedBy("mLock") 234 private boolean mDestroyed; 235 OriginTransitionRecord(RemoteTransition launchTransition, RemoteTransition returnTransition)236 OriginTransitionRecord(RemoteTransition launchTransition, RemoteTransition returnTransition) 237 throws RemoteException { 238 mWrappedLaunchTransition = wrap(launchTransition, this::onLaunchTransitionStarting); 239 mWrappedReturnTransition = wrap(returnTransition, this::onReturnTransitionStarting); 240 linkToDeath(); 241 } 242 onLaunchTransitionStarting(TransitionInfo info)243 private boolean onLaunchTransitionStarting(TransitionInfo info) { 244 synchronized (mLock) { 245 if (mDestroyed) { 246 return false; 247 } 248 TransitionFilter filter = 249 createFilterForReverseTransition( 250 info, /* forPredictiveBackTakeover= */ false); 251 if (filter != null) { 252 if (DEBUG) { 253 Log.d(TAG, "Registering filter " + filter); 254 } 255 mShellTransitions.registerRemote(filter, mWrappedReturnTransition); 256 } 257 TransitionFilter takeoverFilter = 258 createFilterForReverseTransition( 259 info, /* forPredictiveBackTakeover= */ true); 260 if (takeoverFilter != null) { 261 if (DEBUG) { 262 Log.d(TAG, "Registering filter for takeover " + takeoverFilter); 263 } 264 mShellTransitions.registerRemoteForTakeover( 265 takeoverFilter, mWrappedReturnTransition); 266 } 267 return true; 268 } 269 } 270 onReturnTransitionStarting(TransitionInfo info)271 private boolean onReturnTransitionStarting(TransitionInfo info) { 272 synchronized (mLock) { 273 if (mDestroyed) { 274 return false; 275 } 276 // Clean up stuff. 277 destroy(); 278 return true; 279 } 280 } 281 destroy()282 public void destroy() { 283 synchronized (mLock) { 284 if (mDestroyed) { 285 // Already destroyed. 286 return; 287 } 288 if (DEBUG) { 289 Log.d(TAG, "Destroying origin transition record " + this); 290 } 291 mDestroyed = true; 292 unlinkToDeath(); 293 mShellTransitions.unregisterRemote(mWrappedReturnTransition); 294 mRecords.remove(getToken()); 295 } 296 } 297 linkToDeath()298 private void linkToDeath() throws RemoteException { 299 asDelegate(mWrappedLaunchTransition).mTransition.asBinder().linkToDeath(this, 0); 300 asDelegate(mWrappedReturnTransition).mTransition.asBinder().linkToDeath(this, 0); 301 } 302 unlinkToDeath()303 private void unlinkToDeath() { 304 asDelegate(mWrappedLaunchTransition).mTransition.asBinder().unlinkToDeath(this, 0); 305 asDelegate(mWrappedReturnTransition).mTransition.asBinder().unlinkToDeath(this, 0); 306 } 307 getToken()308 public IBinder getToken() { 309 return asLaunchableTransition().asBinder(); 310 } 311 asLaunchableTransition()312 public RemoteTransition asLaunchableTransition() { 313 return mWrappedLaunchTransition; 314 } 315 316 @Override binderDied()317 public void binderDied() { 318 destroy(); 319 } 320 321 @Override toString()322 public String toString() { 323 return "OriginTransitionRecord{launch=" 324 + mWrappedReturnTransition 325 + ", return=" 326 + mWrappedReturnTransition 327 + "}"; 328 } 329 dump(IndentingPrintWriter ipw)330 public void dump(IndentingPrintWriter ipw) { 331 synchronized (mLock) { 332 ipw.println("OriginTransitionRecord"); 333 ipw.increaseIndent(); 334 ipw.println("mDestroyed: " + mDestroyed); 335 ipw.println("Launch transition:"); 336 ipw.increaseIndent(); 337 ipw.println(mWrappedLaunchTransition); 338 ipw.decreaseIndent(); 339 ipw.println("Return transition:"); 340 ipw.increaseIndent(); 341 ipw.println(mWrappedReturnTransition); 342 ipw.decreaseIndent(); 343 ipw.decreaseIndent(); 344 } 345 } 346 asDelegate(RemoteTransition transition)347 private static RemoteTransitionDelegate asDelegate(RemoteTransition transition) { 348 return (RemoteTransitionDelegate) transition.getRemoteTransition(); 349 } 350 wrap( RemoteTransition transition, Predicate<TransitionInfo> onStarting)351 private RemoteTransition wrap( 352 RemoteTransition transition, Predicate<TransitionInfo> onStarting) { 353 return new RemoteTransition( 354 new RemoteTransitionDelegate( 355 mContext.getMainExecutor(), 356 transition.getRemoteTransition(), 357 onStarting), 358 transition.getDebugName()); 359 } 360 361 @Nullable createFilterForReverseTransition( TransitionInfo info, boolean forPredictiveBackTakeover)362 private static TransitionFilter createFilterForReverseTransition( 363 TransitionInfo info, boolean forPredictiveBackTakeover) { 364 TaskInfo launchingTaskInfo = null; 365 TaskInfo launchedTaskInfo = null; 366 ComponentName launchingActivity = null; 367 ComponentName launchedActivity = null; 368 for (Change change : info.getChanges()) { 369 int mode = change.getMode(); 370 TaskInfo taskInfo = change.getTaskInfo(); 371 ComponentName activity = change.getActivityComponent(); 372 if (TransitionUtil.isClosingMode(mode) 373 && launchingTaskInfo == null 374 && taskInfo != null) { 375 // Found the launching task! 376 launchingTaskInfo = taskInfo; 377 } else if (TransitionUtil.isOpeningMode(mode) 378 && launchedTaskInfo == null 379 && taskInfo != null) { 380 // Found the launched task! 381 launchedTaskInfo = taskInfo; 382 } else if (TransitionUtil.isClosingMode(mode) 383 && launchingActivity == null 384 && activity != null) { 385 // Found the launching activity 386 launchingActivity = activity; 387 } else if (TransitionUtil.isOpeningMode(mode) 388 && launchedActivity == null 389 && activity != null) { 390 // Found the launched activity! 391 launchedActivity = activity; 392 } 393 } 394 if (DEBUG) { 395 Log.d( 396 TAG, 397 "createFilterForReverseTransition: forPredictiveBackTakeover=" 398 + forPredictiveBackTakeover 399 + ", launchingTaskInfo=" 400 + launchingTaskInfo 401 + ", launchedTaskInfo=" 402 + launchedTaskInfo 403 + ", launchingActivity=" 404 + launchedActivity 405 + ", launchedActivity=" 406 + launchedActivity); 407 } 408 if (launchingTaskInfo == null && launchingActivity == null) { 409 Log.w( 410 TAG, 411 "createFilterForReverseTransition: unable to find launching task or" 412 + " launching activity!"); 413 return null; 414 } 415 if (launchedTaskInfo == null && launchedActivity == null) { 416 Log.w( 417 TAG, 418 "createFilterForReverseTransition: unable to find launched task or launched" 419 + " activity!"); 420 return null; 421 } 422 if (launchedTaskInfo != null && launchedTaskInfo.launchCookies.isEmpty()) { 423 Log.w( 424 TAG, 425 "createFilterForReverseTransition: skipped - launched task has no launch" 426 + " cookie!"); 427 return null; 428 } 429 if (forPredictiveBackTakeover && launchedTaskInfo == null) { 430 // Predictive back take over currently only support cross-task transition. 431 Log.d( 432 TAG, 433 "createFilterForReverseTransition: skipped - unable to find launched task" 434 + " for predictive back takeover"); 435 return null; 436 } 437 TransitionFilter filter = new TransitionFilter(); 438 if (forPredictiveBackTakeover) { 439 filter.mTypeSet = new int[] {TRANSIT_PREPARE_BACK_NAVIGATION}; 440 } else { 441 filter.mTypeSet = 442 new int[] {TRANSIT_CLOSE, TRANSIT_TO_BACK, TRANSIT_OPEN, TRANSIT_TO_FRONT}; 443 } 444 445 // The opening activity of the return transition must match the activity we just closed. 446 TransitionFilter.Requirement req1 = new TransitionFilter.Requirement(); 447 req1.mModes = new int[] {TRANSIT_OPEN, TRANSIT_TO_FRONT}; 448 req1.mTopActivity = 449 launchingActivity == null ? launchingTaskInfo.topActivity : launchingActivity; 450 451 TransitionFilter.Requirement req2 = new TransitionFilter.Requirement(); 452 if (forPredictiveBackTakeover) { 453 req2.mModes = new int[] {TRANSIT_CHANGE}; 454 } else { 455 req2.mModes = new int[] {TRANSIT_CLOSE, TRANSIT_TO_BACK}; 456 } 457 if (launchedTaskInfo != null) { 458 // For task transitions, the closing task's cookie must match the task we just 459 // launched. 460 req2.mLaunchCookie = launchedTaskInfo.launchCookies.get(0); 461 } else { 462 // For activity transitions, the closing activity of the return transition must 463 // match the activity we just launched. 464 req2.mTopActivity = launchedActivity; 465 } 466 467 filter.mRequirements = new TransitionFilter.Requirement[] {req1, req2}; 468 return filter; 469 } 470 } 471 } 472