1 /* 2 * Copyright (C) 2023 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.desktopmode; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.RectEvaluator; 24 import android.animation.ValueAnimator; 25 import android.app.ActivityManager; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.os.IBinder; 29 import android.util.Slog; 30 import android.view.SurfaceControl; 31 import android.view.WindowManager; 32 import android.window.TransitionInfo; 33 import android.window.TransitionRequestInfo; 34 import android.window.WindowContainerTransaction; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 39 import com.android.wm.shell.transition.Transitions; 40 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration; 41 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.function.Consumer; 46 import java.util.function.Supplier; 47 48 /** 49 * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks 50 * entering and exiting freeform. 51 */ 52 public class EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler { 53 54 private static final String TAG = "EnterDesktopTaskTransitionHandler"; 55 private final Transitions mTransitions; 56 private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; 57 58 // The size of the screen after drag relative to the fullscreen size 59 public static final float FINAL_FREEFORM_SCALE = 0.6f; 60 public static final int FREEFORM_ANIMATION_DURATION = 336; 61 62 private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); 63 private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; 64 private MoveToDesktopAnimator mMoveToDesktopAnimator; 65 private DesktopModeWindowDecoration mDesktopModeWindowDecoration; 66 EnterDesktopTaskTransitionHandler( Transitions transitions)67 public EnterDesktopTaskTransitionHandler( 68 Transitions transitions) { 69 this(transitions, SurfaceControl.Transaction::new); 70 } 71 EnterDesktopTaskTransitionHandler( Transitions transitions, Supplier<SurfaceControl.Transaction> supplier)72 public EnterDesktopTaskTransitionHandler( 73 Transitions transitions, 74 Supplier<SurfaceControl.Transaction> supplier) { 75 mTransitions = transitions; 76 mTransactionSupplier = supplier; 77 } 78 79 /** 80 * Starts Transition of a given type 81 * @param type Transition type 82 * @param wct WindowContainerTransaction for transition 83 * @param onAnimationEndCallback to be called after animation 84 */ startTransition(@indowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)85 private void startTransition(@WindowManager.TransitionType int type, 86 @NonNull WindowContainerTransaction wct, 87 Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { 88 mOnAnimationFinishedCallback = onAnimationEndCallback; 89 final IBinder token = mTransitions.startTransition(type, wct, this); 90 mPendingTransitionTokens.add(token); 91 } 92 93 /** 94 * Starts Transition of type TRANSIT_START_DRAG_TO_DESKTOP_MODE 95 * @param wct WindowContainerTransaction for transition 96 * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move 97 * to desktop animation 98 * @param onAnimationEndCallback to be called after animation 99 */ startMoveToDesktop(@onNull WindowContainerTransaction wct, @NonNull MoveToDesktopAnimator moveToDesktopAnimator, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)100 public void startMoveToDesktop(@NonNull WindowContainerTransaction wct, 101 @NonNull MoveToDesktopAnimator moveToDesktopAnimator, 102 Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { 103 mMoveToDesktopAnimator = moveToDesktopAnimator; 104 startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct, 105 onAnimationEndCallback); 106 } 107 108 /** 109 * Starts Transition of type TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE 110 * @param wct WindowContainerTransaction for transition 111 * @param onAnimationEndCallback to be called after animation 112 */ finalizeMoveToDesktop(@onNull WindowContainerTransaction wct, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)113 public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct, 114 Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { 115 startTransition(Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, wct, 116 onAnimationEndCallback); 117 } 118 119 /** 120 * Starts Transition of type TRANSIT_CANCEL_ENTERING_DESKTOP_MODE 121 * @param wct WindowContainerTransaction for transition 122 * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move 123 * to desktop animation 124 * @param onAnimationEndCallback to be called after animation 125 */ startCancelMoveToDesktopMode(@onNull WindowContainerTransaction wct, MoveToDesktopAnimator moveToDesktopAnimator, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)126 public void startCancelMoveToDesktopMode(@NonNull WindowContainerTransaction wct, 127 MoveToDesktopAnimator moveToDesktopAnimator, 128 Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { 129 mMoveToDesktopAnimator = moveToDesktopAnimator; 130 startTransition(Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE, wct, 131 onAnimationEndCallback); 132 } 133 134 /** 135 * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP 136 * @param wct WindowContainerTransaction for transition 137 * @param decor {@link DesktopModeWindowDecoration} of task being animated 138 */ moveToDesktop(@onNull WindowContainerTransaction wct, DesktopModeWindowDecoration decor)139 public void moveToDesktop(@NonNull WindowContainerTransaction wct, 140 DesktopModeWindowDecoration decor) { 141 mDesktopModeWindowDecoration = decor; 142 startTransition(Transitions.TRANSIT_MOVE_TO_DESKTOP, wct, 143 null /* onAnimationEndCallback */); 144 } 145 146 @Override startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)147 public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, 148 @NonNull SurfaceControl.Transaction startT, 149 @NonNull SurfaceControl.Transaction finishT, 150 @NonNull Transitions.TransitionFinishCallback finishCallback) { 151 boolean transitionHandled = false; 152 for (TransitionInfo.Change change : info.getChanges()) { 153 if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) { 154 continue; 155 } 156 157 final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); 158 if (taskInfo == null || taskInfo.taskId == -1) { 159 continue; 160 } 161 162 if (change.getMode() == WindowManager.TRANSIT_CHANGE) { 163 transitionHandled |= startChangeTransition( 164 transition, info.getType(), change, startT, finishT, finishCallback); 165 } 166 } 167 168 mPendingTransitionTokens.remove(transition); 169 170 return transitionHandled; 171 } 172 startChangeTransition( @onNull IBinder transition, @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)173 private boolean startChangeTransition( 174 @NonNull IBinder transition, 175 @WindowManager.TransitionType int type, 176 @NonNull TransitionInfo.Change change, 177 @NonNull SurfaceControl.Transaction startT, 178 @NonNull SurfaceControl.Transaction finishT, 179 @NonNull Transitions.TransitionFinishCallback finishCallback) { 180 if (!mPendingTransitionTokens.contains(transition)) { 181 return false; 182 } 183 184 final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); 185 if (type == Transitions.TRANSIT_MOVE_TO_DESKTOP 186 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { 187 return animateMoveToDesktop(change, startT, finishCallback); 188 } 189 190 if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE 191 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { 192 return animateStartDragToDesktopMode(change, startT, finishT, finishCallback); 193 } 194 195 final Rect endBounds = change.getEndAbsBounds(); 196 if (type == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE 197 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM 198 && !endBounds.isEmpty()) { 199 return animateFinalizeDragToDesktopMode(change, startT, finishT, finishCallback, 200 endBounds); 201 } 202 203 if (type == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE 204 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { 205 return animateCancelDragToDesktopMode(change, startT, finishT, finishCallback, 206 endBounds); 207 } 208 209 return false; 210 } 211 animateMoveToDesktop( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull Transitions.TransitionFinishCallback finishCallback)212 private boolean animateMoveToDesktop( 213 @NonNull TransitionInfo.Change change, 214 @NonNull SurfaceControl.Transaction startT, 215 @NonNull Transitions.TransitionFinishCallback finishCallback) { 216 if (mDesktopModeWindowDecoration == null) { 217 Slog.e(TAG, "Window Decoration is not available for this transition"); 218 return false; 219 } 220 221 final SurfaceControl leash = change.getLeash(); 222 final Rect startBounds = change.getStartAbsBounds(); 223 startT.setPosition(leash, startBounds.left, startBounds.right) 224 .setWindowCrop(leash, startBounds.width(), startBounds.height()) 225 .show(leash); 226 mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds); 227 228 final ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(), 229 change.getStartAbsBounds(), change.getEndAbsBounds()); 230 animator.setDuration(FREEFORM_ANIMATION_DURATION); 231 SurfaceControl.Transaction t = mTransactionSupplier.get(); 232 animator.addUpdateListener(animation -> { 233 final Rect animationValue = (Rect) animator.getAnimatedValue(); 234 t.setPosition(leash, animationValue.left, animationValue.right) 235 .setWindowCrop(leash, animationValue.width(), animationValue.height()) 236 .show(leash); 237 mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue); 238 }); 239 animator.addListener(new AnimatorListenerAdapter() { 240 @Override 241 public void onAnimationEnd(Animator animation) { 242 mDesktopModeWindowDecoration.hideResizeVeil(); 243 mTransitions.getMainExecutor().execute( 244 () -> finishCallback.onTransitionFinished(null)); 245 } 246 }); 247 animator.start(); 248 return true; 249 } 250 animateStartDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)251 private boolean animateStartDragToDesktopMode( 252 @NonNull TransitionInfo.Change change, 253 @NonNull SurfaceControl.Transaction startT, 254 @NonNull SurfaceControl.Transaction finishT, 255 @NonNull Transitions.TransitionFinishCallback finishCallback) { 256 // Transitioning to freeform but keeping fullscreen bounds, so the crop is set 257 // to null and we don't require an animation 258 final SurfaceControl sc = change.getLeash(); 259 startT.setWindowCrop(sc, null); 260 261 if (mMoveToDesktopAnimator == null 262 || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) { 263 Slog.e(TAG, "No animator available for this transition"); 264 return false; 265 } 266 267 // Calculate and set position of the task 268 final PointF position = mMoveToDesktopAnimator.getPosition(); 269 startT.setPosition(sc, position.x, position.y); 270 finishT.setPosition(sc, position.x, position.y); 271 272 startT.apply(); 273 274 mTransitions.getMainExecutor().execute( 275 () -> finishCallback.onTransitionFinished(null)); 276 277 return true; 278 } 279 animateFinalizeDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull Rect endBounds)280 private boolean animateFinalizeDragToDesktopMode( 281 @NonNull TransitionInfo.Change change, 282 @NonNull SurfaceControl.Transaction startT, 283 @NonNull SurfaceControl.Transaction finishT, 284 @NonNull Transitions.TransitionFinishCallback finishCallback, 285 @NonNull Rect endBounds) { 286 // This Transition animates a task to freeform bounds after being dragged into freeform 287 // mode and brings the remaining freeform tasks to front 288 final SurfaceControl sc = change.getLeash(); 289 startT.setWindowCrop(sc, endBounds.width(), 290 endBounds.height()); 291 startT.apply(); 292 293 // End the animation that shrinks the window when task is first dragged from fullscreen 294 if (mMoveToDesktopAnimator != null) { 295 mMoveToDesktopAnimator.endAnimator(); 296 } 297 298 // We want to find the scale of the current bounds relative to the end bounds. The 299 // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be 300 // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to 301 // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds 302 final ValueAnimator animator = 303 ValueAnimator.ofFloat( 304 MoveToDesktopAnimator.DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f); 305 animator.setDuration(FREEFORM_ANIMATION_DURATION); 306 final SurfaceControl.Transaction t = mTransactionSupplier.get(); 307 animator.addUpdateListener(animation -> { 308 final float animationValue = (float) animation.getAnimatedValue(); 309 t.setScale(sc, animationValue, animationValue); 310 311 final float animationWidth = endBounds.width() * animationValue; 312 final float animationHeight = endBounds.height() * animationValue; 313 final int animationX = endBounds.centerX() - (int) (animationWidth / 2); 314 final int animationY = endBounds.centerY() - (int) (animationHeight / 2); 315 316 t.setPosition(sc, animationX, animationY); 317 t.apply(); 318 }); 319 320 animator.addListener(new AnimatorListenerAdapter() { 321 @Override 322 public void onAnimationEnd(Animator animation) { 323 if (mOnAnimationFinishedCallback != null) { 324 mOnAnimationFinishedCallback.accept(finishT); 325 } 326 mTransitions.getMainExecutor().execute( 327 () -> finishCallback.onTransitionFinished(null)); 328 } 329 }); 330 331 animator.start(); 332 return true; 333 } animateCancelDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull Rect endBounds)334 private boolean animateCancelDragToDesktopMode( 335 @NonNull TransitionInfo.Change change, 336 @NonNull SurfaceControl.Transaction startT, 337 @NonNull SurfaceControl.Transaction finishT, 338 @NonNull Transitions.TransitionFinishCallback finishCallback, 339 @NonNull Rect endBounds) { 340 // This Transition animates a task to fullscreen after being dragged from the status 341 // bar and then released back into the status bar area 342 final SurfaceControl sc = change.getLeash(); 343 // Hide the first (fullscreen) frame because the animation will start from the smaller 344 // scale size. 345 startT.hide(sc) 346 .setWindowCrop(sc, endBounds.width(), endBounds.height()) 347 .apply(); 348 349 if (mMoveToDesktopAnimator == null 350 || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) { 351 Slog.e(TAG, "No animator available for this transition"); 352 return false; 353 } 354 355 // End the animation that shrinks the window when task is first dragged from fullscreen 356 mMoveToDesktopAnimator.endAnimator(); 357 358 final ValueAnimator animator = new ValueAnimator(); 359 animator.setFloatValues(MoveToDesktopAnimator.DRAG_FREEFORM_SCALE, 1f); 360 animator.setDuration(FREEFORM_ANIMATION_DURATION); 361 final SurfaceControl.Transaction t = mTransactionSupplier.get(); 362 363 // Get position of the task 364 final float x = mMoveToDesktopAnimator.getPosition().x; 365 final float y = mMoveToDesktopAnimator.getPosition().y; 366 367 animator.addUpdateListener(animation -> { 368 final float scale = (float) animation.getAnimatedValue(); 369 t.setPosition(sc, x * (1 - scale), y * (1 - scale)) 370 .setScale(sc, scale, scale) 371 .show(sc) 372 .apply(); 373 }); 374 animator.addListener(new AnimatorListenerAdapter() { 375 @Override 376 public void onAnimationEnd(Animator animation) { 377 if (mOnAnimationFinishedCallback != null) { 378 mOnAnimationFinishedCallback.accept(finishT); 379 } 380 mTransitions.getMainExecutor().execute( 381 () -> finishCallback.onTransitionFinished(null)); 382 } 383 }); 384 animator.start(); 385 return true; 386 } 387 388 @Nullable 389 @Override handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)390 public WindowContainerTransaction handleRequest(@NonNull IBinder transition, 391 @NonNull TransitionRequestInfo request) { 392 return null; 393 } 394 } 395