1 /* 2 * Copyright (C) 2022 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.unfold; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; 21 import static android.view.WindowManager.TRANSIT_CHANGE; 22 23 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS; 24 25 import android.animation.ValueAnimator; 26 import android.app.ActivityManager; 27 import android.os.Handler; 28 import android.os.IBinder; 29 import android.util.Slog; 30 import android.view.SurfaceControl; 31 import android.window.TransitionInfo; 32 import android.window.TransitionRequestInfo; 33 import android.window.WindowContainerTransaction; 34 35 import androidx.annotation.IntDef; 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.internal.protolog.ProtoLog; 41 import com.android.wm.shell.bubbles.BubbleTaskUnfoldTransitionMerger; 42 import com.android.wm.shell.shared.TransactionPool; 43 import com.android.wm.shell.shared.TransitionUtil; 44 import com.android.wm.shell.sysui.ShellInit; 45 import com.android.wm.shell.transition.Transitions; 46 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback; 47 import com.android.wm.shell.transition.Transitions.TransitionHandler; 48 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; 49 import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator; 50 import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; 51 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; 52 53 import java.lang.annotation.Retention; 54 import java.lang.annotation.RetentionPolicy; 55 import java.util.ArrayList; 56 import java.util.List; 57 import java.util.Optional; 58 import java.util.concurrent.Executor; 59 60 /** 61 * Transition handler that is responsible for animating app surfaces when unfolding of foldable 62 * devices. It does not handle the folding animation, which is done in 63 * {@link UnfoldAnimationController}. 64 */ 65 public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener { 66 67 private static final String TAG = "UnfoldTransitionHandler"; 68 @VisibleForTesting 69 static final int FINISH_ANIMATION_TIMEOUT_MILLIS = 5_000; 70 71 @Retention(RetentionPolicy.SOURCE) 72 @IntDef({ 73 DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE, 74 DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD, 75 DefaultDisplayChange.DEFAULT_DISPLAY_FOLD, 76 }) 77 private @interface DefaultDisplayChange { 78 int DEFAULT_DISPLAY_NO_CHANGE = 0; 79 int DEFAULT_DISPLAY_UNFOLD = 1; 80 int DEFAULT_DISPLAY_FOLD = 2; 81 } 82 83 private final ShellUnfoldProgressProvider mUnfoldProgressProvider; 84 private final Transitions mTransitions; 85 private final Optional<BubbleTaskUnfoldTransitionMerger> mBubbleTaskUnfoldTransitionMerger; 86 private final Executor mExecutor; 87 private final TransactionPool mTransactionPool; 88 private final Handler mHandler; 89 90 @Nullable 91 private TransitionFinishCallback mFinishCallback; 92 @Nullable 93 private IBinder mTransition; 94 95 // TODO: b/318803244 - remove when we could guarantee finishing the animation 96 // after startAnimation callback 97 private boolean mAnimationFinished = false; 98 private float mLastAnimationProgress = 0.0f; 99 private final List<UnfoldTaskAnimator> mAnimators = new ArrayList<>(); 100 101 private final Runnable mAnimationPlayingTimeoutRunnable = () -> { 102 Slog.wtf(TAG, "Timeout occurred when playing the unfold animation, " 103 + "force finishing the transition"); 104 finishTransitionIfNeeded(); 105 }; 106 UnfoldTransitionHandler(ShellInit shellInit, ShellUnfoldProgressProvider unfoldProgressProvider, FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator, SplitTaskUnfoldAnimator splitUnfoldTaskAnimator, TransactionPool transactionPool, Executor executor, Handler handler, Transitions transitions, Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger)107 public UnfoldTransitionHandler(ShellInit shellInit, 108 ShellUnfoldProgressProvider unfoldProgressProvider, 109 FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator, 110 SplitTaskUnfoldAnimator splitUnfoldTaskAnimator, 111 TransactionPool transactionPool, 112 Executor executor, 113 Handler handler, 114 Transitions transitions, 115 Optional<BubbleTaskUnfoldTransitionMerger> bubbleTaskUnfoldTransitionMerger) { 116 mUnfoldProgressProvider = unfoldProgressProvider; 117 mTransitions = transitions; 118 mTransactionPool = transactionPool; 119 mExecutor = executor; 120 mHandler = handler; 121 mBubbleTaskUnfoldTransitionMerger = bubbleTaskUnfoldTransitionMerger; 122 123 mAnimators.add(splitUnfoldTaskAnimator); 124 mAnimators.add(fullscreenUnfoldAnimator); 125 // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic 126 // override for this controller from the base module 127 if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER 128 && Transitions.ENABLE_SHELL_TRANSITIONS) { 129 shellInit.addInitCallback(this::onInit, this); 130 } 131 } 132 133 /** 134 * Called when the transition handler is initialized. 135 */ onInit()136 public void onInit() { 137 for (int i = 0; i < mAnimators.size(); i++) { 138 mAnimators.get(i).init(); 139 } 140 mTransitions.addHandler(this); 141 mUnfoldProgressProvider.addListener(mExecutor, this); 142 } 143 144 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback)145 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 146 @NonNull SurfaceControl.Transaction startTransaction, 147 @NonNull SurfaceControl.Transaction finishTransaction, 148 @NonNull TransitionFinishCallback finishCallback) { 149 if (transition != mTransition) return false; 150 151 for (int i = 0; i < mAnimators.size(); i++) { 152 final UnfoldTaskAnimator animator = mAnimators.get(i); 153 animator.clearTasks(); 154 155 info.getChanges().forEach(change -> { 156 if (change.getTaskInfo() != null) { 157 ProtoLog.v(WM_SHELL_TRANSITIONS, 158 "startAnimation, check taskInfo: %s, mode: %s, isApplicableTask: %s", 159 change.getTaskInfo(), TransitionInfo.modeToString(change.getMode()), 160 animator.isApplicableTask(change.getTaskInfo())); 161 } 162 if (change.getTaskInfo() != null && (change.getMode() == TRANSIT_CHANGE 163 || TransitionUtil.isOpeningType(change.getMode())) 164 && animator.isApplicableTask(change.getTaskInfo())) { 165 animator.onTaskAppeared(change.getTaskInfo(), change.getLeash()); 166 } 167 }); 168 169 if (animator.hasActiveTasks()) { 170 animator.prepareStartTransaction(startTransaction); 171 animator.prepareFinishTransaction(finishTransaction); 172 animator.start(); 173 } 174 } 175 176 startTransaction.apply(); 177 mFinishCallback = finishCallback; 178 179 // Shell transition started when unfold animation has already finished, 180 // finish shell transition immediately 181 if (mAnimationFinished) { 182 finishTransitionIfNeeded(); 183 } else { 184 // TODO: b/318803244 - remove timeout handling when we could guarantee that 185 // the animation will be always finished after receiving startAnimation 186 mHandler.removeCallbacks(mAnimationPlayingTimeoutRunnable); 187 mHandler.postDelayed(mAnimationPlayingTimeoutRunnable, FINISH_ANIMATION_TIMEOUT_MILLIS); 188 } 189 190 return true; 191 } 192 193 @Override onStateChangeProgress(float progress)194 public void onStateChangeProgress(float progress) { 195 mLastAnimationProgress = progress; 196 197 if (mTransition == null) return; 198 199 SurfaceControl.Transaction transaction = null; 200 201 for (int i = 0; i < mAnimators.size(); i++) { 202 final UnfoldTaskAnimator animator = mAnimators.get(i); 203 204 if (animator.hasActiveTasks()) { 205 if (transaction == null) { 206 transaction = mTransactionPool.acquire(); 207 } 208 209 animator.applyAnimationProgress(progress, transaction); 210 } 211 } 212 213 if (transaction != null) { 214 transaction.apply(); 215 mTransactionPool.release(transaction); 216 } 217 } 218 219 @Override onStateChangeFinished()220 public void onStateChangeFinished() { 221 finishTransitionIfNeeded(); 222 223 // mLastAnimationProgress is guaranteed to be 0f when folding finishes, see 224 // {@link PhysicsBasedUnfoldTransitionProgressProvider#cancelTransition}. 225 // We can use it as an indication that the next animation progress events will be related 226 // to unfolding, so let's reset mAnimationFinished to 'false' in this case. 227 final boolean isFoldingFinished = mLastAnimationProgress == 0f; 228 mAnimationFinished = !isFoldingFinished; 229 } 230 231 @Override mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback)232 public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 233 @NonNull SurfaceControl.Transaction startT, 234 @NonNull SurfaceControl.Transaction finishT, 235 @NonNull IBinder mergeTarget, 236 @NonNull TransitionFinishCallback finishCallback) { 237 if (info.getType() != TRANSIT_CHANGE) { 238 return; 239 } 240 if ((info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) { 241 return; 242 } 243 // TODO (b/286928742) unfold transition handler should be part of mixed handler to 244 // handle merges better. 245 246 for (int i = 0; i < info.getChanges().size(); ++i) { 247 final TransitionInfo.Change change = info.getChanges().get(i); 248 final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); 249 if (taskInfo != null 250 && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) { 251 // Tasks that are always on top, excluding bubbles, will handle their own transition 252 // as they are on top of everything else. If this is a transition for a bubble task, 253 // attempt to merge it. Otherwise skip merging transitions. 254 if (mBubbleTaskUnfoldTransitionMerger.isPresent()) { 255 boolean merged = 256 mBubbleTaskUnfoldTransitionMerger 257 .get() 258 .mergeTaskWithUnfold(taskInfo, change, startT, finishT); 259 if (!merged) { 260 return; 261 } 262 } else { 263 return; 264 } 265 } 266 } 267 // Apply changes happening during the unfold animation immediately 268 startT.apply(); 269 finishCallback.onTransitionFinished(null); 270 271 if (getDefaultDisplayChange(info) == DefaultDisplayChange.DEFAULT_DISPLAY_FOLD) { 272 // Force-finish current unfold animation as we are processing folding now which doesn't 273 // have any animations on the Shell side 274 finishTransitionIfNeeded(); 275 } 276 } 277 278 /** Whether `request` contains an unfold action. */ shouldPlayUnfoldAnimation(@onNull TransitionRequestInfo request)279 public boolean shouldPlayUnfoldAnimation(@NonNull TransitionRequestInfo request) { 280 // Unfold animation won't play when animations are disabled 281 if (!ValueAnimator.areAnimatorsEnabled()) return false; 282 283 return (request.getType() == TRANSIT_CHANGE 284 && getDefaultDisplayChange(request.getDisplayChange()) 285 == DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD); 286 } 287 288 @DefaultDisplayChange getDefaultDisplayChange( @ullable TransitionRequestInfo.DisplayChange displayChange)289 private int getDefaultDisplayChange( 290 @Nullable TransitionRequestInfo.DisplayChange displayChange) { 291 if (displayChange == null) return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE; 292 293 if (displayChange.getDisplayId() != DEFAULT_DISPLAY) { 294 return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE; 295 } 296 297 if (!displayChange.isPhysicalDisplayChanged()) { 298 return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE; 299 } 300 301 if (displayChange.getStartAbsBounds() == null || displayChange.getEndAbsBounds() == null) { 302 return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE; 303 } 304 305 // Handle only unfolding, currently we don't have an animation when folding 306 final int endArea = 307 displayChange.getEndAbsBounds().width() * displayChange.getEndAbsBounds().height(); 308 final int startArea = displayChange.getStartAbsBounds().width() 309 * displayChange.getStartAbsBounds().height(); 310 311 return endArea > startArea ? DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD 312 : DefaultDisplayChange.DEFAULT_DISPLAY_FOLD; 313 } 314 getDefaultDisplayChange(@onNull TransitionInfo transitionInfo)315 private int getDefaultDisplayChange(@NonNull TransitionInfo transitionInfo) { 316 for (int i = 0; i < transitionInfo.getChanges().size(); i++) { 317 final TransitionInfo.Change change = transitionInfo.getChanges().get(i); 318 // We are interested only in display container changes 319 if ((change.getFlags() & TransitionInfo.FLAG_IS_DISPLAY) == 0) { 320 continue; 321 } 322 323 // Handle only unfolding, currently we don't have an animation when folding 324 if (change.getEndAbsBounds() == null || change.getStartAbsBounds() == null) { 325 continue; 326 } 327 328 final int afterArea = 329 change.getEndAbsBounds().width() * change.getEndAbsBounds().height(); 330 final int beforeArea = change.getStartAbsBounds().width() 331 * change.getStartAbsBounds().height(); 332 333 if (afterArea > beforeArea) { 334 return DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD; 335 } else { 336 return DefaultDisplayChange.DEFAULT_DISPLAY_FOLD; 337 } 338 } 339 340 return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE; 341 } 342 343 @Nullable 344 @Override handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)345 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 346 @NonNull TransitionRequestInfo request) { 347 if (shouldPlayUnfoldAnimation(request)) { 348 mTransition = transition; 349 return new WindowContainerTransaction(); 350 } 351 return null; 352 } 353 willHandleTransition()354 public boolean willHandleTransition() { 355 return mTransition != null; 356 } 357 358 @Override onFoldStateChanged(boolean isFolded)359 public void onFoldStateChanged(boolean isFolded) { 360 if (isFolded) { 361 // If we are currently animating unfold animation we should finish it because 362 // the animation might not start and finish as the device was folded 363 finishTransitionIfNeeded(); 364 } 365 } 366 finishTransitionIfNeeded()367 private void finishTransitionIfNeeded() { 368 if (mFinishCallback == null) return; 369 370 for (int i = 0; i < mAnimators.size(); i++) { 371 final UnfoldTaskAnimator animator = mAnimators.get(i); 372 animator.clearTasks(); 373 animator.stop(); 374 } 375 376 mHandler.removeCallbacks(mAnimationPlayingTimeoutRunnable); 377 mFinishCallback.onTransitionFinished(null); 378 mFinishCallback = null; 379 mTransition = null; 380 } 381 } 382