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 com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.RectEvaluator; 24 import android.animation.ValueAnimator; 25 import android.annotation.NonNull; 26 import android.app.ActivityManager; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.PixelFormat; 30 import android.graphics.PointF; 31 import android.graphics.Rect; 32 import android.util.DisplayMetrics; 33 import android.view.SurfaceControl; 34 import android.view.SurfaceControlViewHost; 35 import android.view.View; 36 import android.view.WindowManager; 37 import android.view.WindowlessWindowManager; 38 import android.view.animation.DecelerateInterpolator; 39 40 import com.android.wm.shell.R; 41 import com.android.wm.shell.RootTaskDisplayAreaOrganizer; 42 import com.android.wm.shell.ShellTaskOrganizer; 43 import com.android.wm.shell.common.DisplayController; 44 import com.android.wm.shell.common.DisplayLayout; 45 import com.android.wm.shell.common.SyncTransactionQueue; 46 47 /** 48 * Animated visual indicator for Desktop Mode windowing transitions. 49 */ 50 public class DesktopModeVisualIndicator { 51 public static final int INVALID_INDICATOR = -1; 52 /** Indicates impending transition into desktop mode */ 53 public static final int TO_DESKTOP_INDICATOR = 1; 54 /** Indicates impending transition into fullscreen */ 55 public static final int TO_FULLSCREEN_INDICATOR = 2; 56 /** Indicates impending transition into split select on the left side */ 57 public static final int TO_SPLIT_LEFT_INDICATOR = 3; 58 /** Indicates impending transition into split select on the right side */ 59 public static final int TO_SPLIT_RIGHT_INDICATOR = 4; 60 61 private final Context mContext; 62 private final DisplayController mDisplayController; 63 private final ShellTaskOrganizer mTaskOrganizer; 64 private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; 65 private final ActivityManager.RunningTaskInfo mTaskInfo; 66 private final SurfaceControl mTaskSurface; 67 private final Rect mIndicatorRange = new Rect(); 68 private SurfaceControl mLeash; 69 70 private final SyncTransactionQueue mSyncQueue; 71 private SurfaceControlViewHost mViewHost; 72 73 private View mView; 74 private boolean mIsFullscreen; 75 private int mType; 76 DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, int type)77 public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, 78 ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, 79 Context context, SurfaceControl taskSurface, ShellTaskOrganizer taskOrganizer, 80 RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, int type) { 81 mSyncQueue = syncQueue; 82 mTaskInfo = taskInfo; 83 mDisplayController = displayController; 84 mContext = context; 85 mTaskSurface = taskSurface; 86 mTaskOrganizer = taskOrganizer; 87 mRootTdaOrganizer = taskDisplayAreaOrganizer; 88 mType = type; 89 defineIndicatorRange(); 90 createView(); 91 } 92 93 /** 94 * If an indicator is warranted based on the input and task bounds, return the type of 95 * indicator that should be created. 96 */ determineIndicatorType(PointF inputCoordinates, Rect taskBounds, DisplayLayout layout, Context context)97 public static int determineIndicatorType(PointF inputCoordinates, Rect taskBounds, 98 DisplayLayout layout, Context context) { 99 int transitionAreaHeight = context.getResources().getDimensionPixelSize( 100 com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); 101 int transitionAreaWidth = context.getResources().getDimensionPixelSize( 102 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); 103 if (taskBounds.top <= transitionAreaHeight) return TO_FULLSCREEN_INDICATOR; 104 if (inputCoordinates.x <= transitionAreaWidth) return TO_SPLIT_LEFT_INDICATOR; 105 if (inputCoordinates.x >= layout.width() - transitionAreaWidth) { 106 return TO_SPLIT_RIGHT_INDICATOR; 107 } 108 return INVALID_INDICATOR; 109 } 110 111 /** 112 * Determine range of inputs that will keep this indicator displaying. 113 */ defineIndicatorRange()114 private void defineIndicatorRange() { 115 DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); 116 int captionHeight = mContext.getResources().getDimensionPixelSize( 117 com.android.wm.shell.R.dimen.freeform_decor_caption_height); 118 int transitionAreaHeight = mContext.getResources().getDimensionPixelSize( 119 com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); 120 int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( 121 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); 122 switch (mType) { 123 case TO_DESKTOP_INDICATOR: 124 // TO_DESKTOP indicator is only dismissed on release; entire display is valid. 125 mIndicatorRange.set(0, 0, layout.width(), layout.height()); 126 break; 127 case TO_FULLSCREEN_INDICATOR: 128 // If drag results in caption going above the top edge of the display, we still 129 // want to transition to fullscreen. 130 mIndicatorRange.set(0, -captionHeight, layout.width(), transitionAreaHeight); 131 break; 132 case TO_SPLIT_LEFT_INDICATOR: 133 mIndicatorRange.set(0, transitionAreaHeight, transitionAreaWidth, layout.height()); 134 break; 135 case TO_SPLIT_RIGHT_INDICATOR: 136 mIndicatorRange.set(layout.width() - transitionAreaWidth, transitionAreaHeight, 137 layout.width(), layout.height()); 138 break; 139 default: 140 break; 141 } 142 } 143 144 145 /** 146 * Create a fullscreen indicator with no animation 147 */ createView()148 private void createView() { 149 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 150 final Resources resources = mContext.getResources(); 151 final DisplayMetrics metrics = resources.getDisplayMetrics(); 152 final int screenWidth = metrics.widthPixels; 153 final int screenHeight = metrics.heightPixels; 154 155 mView = new View(mContext); 156 final SurfaceControl.Builder builder = new SurfaceControl.Builder(); 157 mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); 158 String description; 159 switch (mType) { 160 case TO_DESKTOP_INDICATOR: 161 description = "Desktop indicator"; 162 break; 163 case TO_FULLSCREEN_INDICATOR: 164 description = "Fullscreen indicator"; 165 break; 166 case TO_SPLIT_LEFT_INDICATOR: 167 description = "Split Left indicator"; 168 break; 169 case TO_SPLIT_RIGHT_INDICATOR: 170 description = "Split Right indicator"; 171 break; 172 default: 173 description = "Invalid indicator"; 174 break; 175 } 176 mLeash = builder 177 .setName(description) 178 .setContainerLayer() 179 .build(); 180 t.show(mLeash); 181 final WindowManager.LayoutParams lp = 182 new WindowManager.LayoutParams(screenWidth, screenHeight, 183 WindowManager.LayoutParams.TYPE_APPLICATION, 184 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); 185 lp.setTitle(description + " for Task=" + mTaskInfo.taskId); 186 lp.setTrustedOverlay(); 187 final WindowlessWindowManager windowManager = new WindowlessWindowManager( 188 mTaskInfo.configuration, mLeash, 189 null /* hostInputToken */); 190 mViewHost = new SurfaceControlViewHost(mContext, 191 mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, 192 "DesktopModeVisualIndicator"); 193 mViewHost.setView(mView, lp); 194 // We want this indicator to be behind the dragged task, but in front of all others. 195 t.setRelativeLayer(mLeash, mTaskSurface, -1); 196 197 mSyncQueue.runInSync(transaction -> { 198 transaction.merge(t); 199 t.close(); 200 }); 201 } 202 203 /** 204 * Create an indicator. Animator fades it in while expanding the bounds outwards. 205 */ createIndicatorWithAnimatedBounds()206 public void createIndicatorWithAnimatedBounds() { 207 mIsFullscreen = mType == TO_FULLSCREEN_INDICATOR; 208 mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background); 209 final VisualIndicatorAnimator animator = VisualIndicatorAnimator 210 .animateBounds(mView, mType, 211 mDisplayController.getDisplayLayout(mTaskInfo.displayId)); 212 animator.start(); 213 } 214 215 /** 216 * Takes existing fullscreen indicator and animates it to freeform bounds 217 */ transitionFullscreenIndicatorToFreeform()218 public void transitionFullscreenIndicatorToFreeform() { 219 mIsFullscreen = false; 220 mType = TO_DESKTOP_INDICATOR; 221 final VisualIndicatorAnimator animator = VisualIndicatorAnimator.toFreeformAnimator( 222 mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId)); 223 animator.start(); 224 } 225 226 /** 227 * Takes the existing freeform indicator and animates it to fullscreen 228 */ transitionFreeformIndicatorToFullscreen()229 public void transitionFreeformIndicatorToFullscreen() { 230 mIsFullscreen = true; 231 mType = TO_FULLSCREEN_INDICATOR; 232 final VisualIndicatorAnimator animator = 233 VisualIndicatorAnimator.toFullscreenAnimatorWithAnimatedBounds( 234 mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId)); 235 animator.start(); 236 } 237 238 /** 239 * Determine if a MotionEvent is in the same range that enabled the indicator. 240 * Used to dismiss the indicator when a transition will no longer result from releasing. 241 */ eventOutsideRange(float x, float y)242 public boolean eventOutsideRange(float x, float y) { 243 return !mIndicatorRange.contains((int) x, (int) y); 244 } 245 246 /** 247 * Release the indicator and its components when it is no longer needed. 248 */ releaseVisualIndicator(SurfaceControl.Transaction t)249 public void releaseVisualIndicator(SurfaceControl.Transaction t) { 250 if (mViewHost == null) return; 251 if (mViewHost != null) { 252 mViewHost.release(); 253 mViewHost = null; 254 } 255 256 if (mLeash != null) { 257 t.remove(mLeash); 258 mLeash = null; 259 } 260 } 261 262 /** 263 * Returns true if visual indicator is fullscreen 264 */ isFullscreen()265 public boolean isFullscreen() { 266 return mIsFullscreen; 267 } 268 269 /** 270 * Animator for Desktop Mode transitions which supports bounds and alpha animation. 271 */ 272 private static class VisualIndicatorAnimator extends ValueAnimator { 273 private static final int FULLSCREEN_INDICATOR_DURATION = 200; 274 private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f; 275 private static final float INDICATOR_FINAL_OPACITY = 0.7f; 276 277 private final View mView; 278 private final Rect mStartBounds; 279 private final Rect mEndBounds; 280 private final RectEvaluator mRectEvaluator; 281 VisualIndicatorAnimator(View view, Rect startBounds, Rect endBounds)282 private VisualIndicatorAnimator(View view, Rect startBounds, 283 Rect endBounds) { 284 mView = view; 285 mStartBounds = new Rect(startBounds); 286 mEndBounds = endBounds; 287 setFloatValues(0, 1); 288 mRectEvaluator = new RectEvaluator(new Rect()); 289 } 290 291 /** 292 * Create animator for visual indicator of fullscreen transition 293 * 294 * @param view the view for this indicator 295 * @param displayLayout information about the display the transitioning task is currently on 296 */ toFullscreenAnimatorWithAnimatedBounds( @onNull View view, @NonNull DisplayLayout displayLayout)297 public static VisualIndicatorAnimator toFullscreenAnimatorWithAnimatedBounds( 298 @NonNull View view, @NonNull DisplayLayout displayLayout) { 299 final int padding = displayLayout.stableInsets().top; 300 Rect startBounds = new Rect(padding, padding, 301 displayLayout.width() - padding, displayLayout.height() - padding); 302 view.getBackground().setBounds(startBounds); 303 304 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 305 view, startBounds, getMaxBounds(startBounds)); 306 animator.setInterpolator(new DecelerateInterpolator()); 307 setupIndicatorAnimation(animator); 308 return animator; 309 } 310 animateBounds( @onNull View view, int type, @NonNull DisplayLayout displayLayout)311 public static VisualIndicatorAnimator animateBounds( 312 @NonNull View view, int type, @NonNull DisplayLayout displayLayout) { 313 final int padding = displayLayout.stableInsets().top; 314 Rect startBounds = new Rect(); 315 switch (type) { 316 case TO_FULLSCREEN_INDICATOR: 317 startBounds.set(padding, padding, 318 displayLayout.width() - padding, 319 displayLayout.height() - padding); 320 break; 321 case TO_SPLIT_LEFT_INDICATOR: 322 startBounds.set(padding, padding, 323 displayLayout.width() / 2 - padding, 324 displayLayout.height() - padding); 325 break; 326 case TO_SPLIT_RIGHT_INDICATOR: 327 startBounds.set(displayLayout.width() / 2 + padding, padding, 328 displayLayout.width() - padding, 329 displayLayout.height() - padding); 330 break; 331 } 332 view.getBackground().setBounds(startBounds); 333 334 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 335 view, startBounds, getMaxBounds(startBounds)); 336 animator.setInterpolator(new DecelerateInterpolator()); 337 setupIndicatorAnimation(animator); 338 return animator; 339 } 340 341 /** 342 * Create animator for visual indicator of freeform transition 343 * 344 * @param view the view for this indicator 345 * @param displayLayout information about the display the transitioning task is currently on 346 */ toFreeformAnimator(@onNull View view, @NonNull DisplayLayout displayLayout)347 public static VisualIndicatorAnimator toFreeformAnimator(@NonNull View view, 348 @NonNull DisplayLayout displayLayout) { 349 final float adjustmentPercentage = 1f - FINAL_FREEFORM_SCALE; 350 final int width = displayLayout.width(); 351 final int height = displayLayout.height(); 352 Rect startBounds = new Rect(0, 0, width, height); 353 Rect endBounds = new Rect((int) (adjustmentPercentage * width / 2), 354 (int) (adjustmentPercentage * height / 2), 355 (int) (displayLayout.width() - (adjustmentPercentage * width / 2)), 356 (int) (displayLayout.height() - (adjustmentPercentage * height / 2))); 357 final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( 358 view, startBounds, endBounds); 359 animator.setInterpolator(new DecelerateInterpolator()); 360 setupIndicatorAnimation(animator); 361 return animator; 362 } 363 364 /** 365 * Add necessary listener for animation of indicator 366 */ setupIndicatorAnimation(@onNull VisualIndicatorAnimator animator)367 private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator) { 368 animator.addUpdateListener(a -> { 369 if (animator.mView != null) { 370 animator.updateBounds(a.getAnimatedFraction(), animator.mView); 371 animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); 372 } else { 373 animator.cancel(); 374 } 375 }); 376 animator.addListener(new AnimatorListenerAdapter() { 377 @Override 378 public void onAnimationEnd(Animator animation) { 379 animator.mView.getBackground().setBounds(animator.mEndBounds); 380 } 381 }); 382 animator.setDuration(FULLSCREEN_INDICATOR_DURATION); 383 } 384 385 /** 386 * Update bounds of view based on current animation fraction. 387 * Use of delta is to animate bounds independently, in case we need to 388 * run multiple animations simultaneously. 389 * 390 * @param fraction fraction to use, compared against previous fraction 391 * @param view the view to update 392 */ updateBounds(float fraction, View view)393 private void updateBounds(float fraction, View view) { 394 if (mStartBounds.equals(mEndBounds)) { 395 return; 396 } 397 Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); 398 view.getBackground().setBounds(currentBounds); 399 } 400 401 /** 402 * Fade in the fullscreen indicator 403 * 404 * @param fraction current animation fraction 405 */ updateIndicatorAlpha(float fraction, View view)406 private void updateIndicatorAlpha(float fraction, View view) { 407 view.setAlpha(fraction * INDICATOR_FINAL_OPACITY); 408 } 409 410 /** 411 * Return the max bounds of a visual indicator 412 */ getMaxBounds(Rect startBounds)413 private static Rect getMaxBounds(Rect startBounds) { 414 return new Rect((int) (startBounds.left 415 - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), 416 (int) (startBounds.top 417 - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())), 418 (int) (startBounds.right 419 + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())), 420 (int) (startBounds.bottom 421 + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height()))); 422 } 423 } 424 } 425