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 package com.android.quickstep; 17 18 import static com.android.app.animation.Interpolators.INSTANT; 19 import static com.android.app.animation.Interpolators.LINEAR; 20 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR; 21 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK; 22 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; 23 24 import android.animation.Animator; 25 import android.animation.ObjectAnimator; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Color; 29 import android.graphics.PointF; 30 import android.graphics.Rect; 31 import android.view.Gravity; 32 import android.view.MotionEvent; 33 import android.view.RemoteAnimationTarget; 34 import android.view.View; 35 36 import androidx.annotation.Nullable; 37 import androidx.annotation.UiThread; 38 39 import com.android.launcher3.DeviceProfile; 40 import com.android.launcher3.Flags; 41 import com.android.launcher3.R; 42 import com.android.launcher3.statehandlers.DesktopVisibilityController; 43 import com.android.launcher3.statemanager.BaseState; 44 import com.android.launcher3.statemanager.StatefulContainer; 45 import com.android.launcher3.taskbar.TaskbarUIController; 46 import com.android.launcher3.util.DisplayController; 47 import com.android.launcher3.util.WindowBounds; 48 import com.android.launcher3.views.ScrimView; 49 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 50 import com.android.quickstep.util.AnimatorControllerWithResistance; 51 import com.android.quickstep.util.ContextInitListener; 52 import com.android.quickstep.views.RecentsView; 53 import com.android.quickstep.views.RecentsViewContainer; 54 import com.android.systemui.shared.recents.model.ThumbnailData; 55 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.function.Consumer; 59 import java.util.function.Predicate; 60 61 public abstract class BaseContainerInterface<STATE_TYPE extends BaseState<STATE_TYPE>, 62 CONTAINER_TYPE extends RecentsViewContainer & StatefulContainer<STATE_TYPE>> { 63 64 public boolean rotationSupportedByActivity = false; 65 protected final STATE_TYPE mBackgroundState; 66 BaseContainerInterface(STATE_TYPE backgroundState)67 protected BaseContainerInterface(STATE_TYPE backgroundState) { 68 mBackgroundState = backgroundState; 69 } 70 71 @UiThread 72 @Nullable getVisibleRecentsView()73 public abstract <T extends RecentsView<?,?>> T getVisibleRecentsView(); 74 75 @UiThread switchToRecentsIfVisible(Animator.AnimatorListener animatorListener)76 public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener); 77 78 @Nullable getCreatedContainer()79 public abstract CONTAINER_TYPE getCreatedContainer(); 80 81 @Nullable 82 protected Runnable mOnInitBackgroundStateUICallback = null; 83 isInLiveTileMode()84 public abstract boolean isInLiveTileMode(); 85 onAssistantVisibilityChanged(float assistantVisibility)86 public abstract void onAssistantVisibilityChanged(float assistantVisibility); 87 isResumed()88 public abstract boolean isResumed(); 89 isStarted()90 public abstract boolean isStarted(); deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev)91 public abstract boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, 92 MotionEvent ev); 93 94 /** 95 * Returns the color of the scrim behind overview when at rest in this state. 96 * Return {@link Color#TRANSPARENT} for no scrim. 97 */ getOverviewScrimColorForState(CONTAINER_TYPE container, STATE_TYPE state)98 protected abstract int getOverviewScrimColorForState(CONTAINER_TYPE container, 99 STATE_TYPE state); 100 getSwipeUpDestinationAndLength( DeviceProfile dp, Context context, Rect outRect, RecentsPagedOrientationHandler orientationHandler)101 public abstract int getSwipeUpDestinationAndLength( 102 DeviceProfile dp, Context context, Rect outRect, 103 RecentsPagedOrientationHandler orientationHandler); 104 105 @Nullable getTaskbarController()106 public abstract TaskbarUIController getTaskbarController(); 107 108 public interface AnimationFactory { 109 createContainerInterface(long transitionLength)110 void createContainerInterface(long transitionLength); 111 112 /** 113 * @param attached Whether to show RecentsView alongside the app window. If false, recents 114 * will be hidden by some property we can animate, e.g. alpha. 115 * @param animate Whether to animate recents to/from its new attached state. 116 * @param updateRunningTaskAlpha Whether to update the running task's attached alpha 117 */ setRecentsAttachedToAppWindow( boolean attached, boolean animate, boolean updateRunningTaskAlpha)118 default void setRecentsAttachedToAppWindow( 119 boolean attached, boolean animate, boolean updateRunningTaskAlpha) { } 120 isRecentsAttachedToAppWindow()121 default boolean isRecentsAttachedToAppWindow() { 122 return false; 123 } 124 hasRecentsEverAttachedToAppWindow()125 default boolean hasRecentsEverAttachedToAppWindow() { 126 return false; 127 } 128 129 /** Called when the gesture ends and we know what state it is going towards */ setEndTarget(GestureState.GestureEndTarget endTarget)130 default void setEndTarget(GestureState.GestureEndTarget endTarget) { } 131 } 132 prepareRecentsUI( boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback)133 public abstract BaseContainerInterface.AnimationFactory prepareRecentsUI( 134 boolean activityVisible, 135 Consumer<AnimatorControllerWithResistance> callback); 136 createActivityInitListener( Predicate<Boolean> onInitListener)137 public abstract ContextInitListener createActivityInitListener( 138 Predicate<Boolean> onInitListener); 139 /** 140 * Returns the expected STATE_TYPE from the provided GestureEndTarget. 141 */ stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget)142 public abstract STATE_TYPE stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget); 143 switchRunningTaskViewToScreenshot(HashMap<Integer, ThumbnailData> thumbnailDatas, Runnable runnable)144 public abstract void switchRunningTaskViewToScreenshot(HashMap<Integer, 145 ThumbnailData> thumbnailDatas, Runnable runnable); 146 closeOverlay()147 public abstract void closeOverlay(); 148 getOverviewWindowBounds( Rect homeBounds, RemoteAnimationTarget target)149 public abstract Rect getOverviewWindowBounds( 150 Rect homeBounds, RemoteAnimationTarget target); 151 onLaunchTaskFailed()152 public abstract void onLaunchTaskFailed(); 153 onExitOverview(Runnable exitRunnable)154 public abstract void onExitOverview(Runnable exitRunnable); 155 156 /** Called when the animation to home has fully settled. */ onSwipeUpToHomeComplete()157 public void onSwipeUpToHomeComplete() {} 158 159 /** 160 * Sets a callback to be run when an activity launch happens while launcher is not yet resumed. 161 */ setOnDeferredActivityLaunchCallback(Runnable r)162 public void setOnDeferredActivityLaunchCallback(Runnable r) {} 163 /** 164 * @return Whether the gesture in progress should be cancelled. 165 */ shouldCancelCurrentGesture()166 public boolean shouldCancelCurrentGesture() { 167 return false; 168 } 169 runOnInitBackgroundStateUI(Runnable callback)170 public void runOnInitBackgroundStateUI(Runnable callback) { 171 StatefulContainer container = getCreatedContainer(); 172 if (container != null 173 && container.getStateManager().getState() == mBackgroundState) { 174 callback.run(); 175 onInitBackgroundStateUI(); 176 return; 177 } 178 mOnInitBackgroundStateUICallback = callback; 179 } 180 181 /** 182 * Called when the gesture ends and the animation starts towards the given target. Used to add 183 * an optional additional animation with the same duration. 184 */ getParallelAnimationToGestureEndTarget( GestureState.GestureEndTarget endTarget, long duration, RecentsAnimationCallbacks callbacks)185 public @Nullable Animator getParallelAnimationToGestureEndTarget( 186 GestureState.GestureEndTarget endTarget, long duration, 187 RecentsAnimationCallbacks callbacks) { 188 if (endTarget == RECENTS) { 189 CONTAINER_TYPE container = getCreatedContainer(); 190 if (container == null) { 191 return null; 192 } 193 RecentsView recentsView = container.getOverviewPanel(); 194 STATE_TYPE state = stateFromGestureEndTarget(endTarget); 195 ScrimView scrimView = container.getScrimView(); 196 ObjectAnimator anim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR, 197 getOverviewScrimColorForState(container, state)); 198 anim.setDuration(duration); 199 anim.setInterpolator(recentsView == null || !recentsView.isKeyboardTaskFocusPending() 200 ? LINEAR : INSTANT); 201 return anim; 202 } 203 return null; 204 } 205 206 /** 207 * Called when the animation to the target has finished, but right before updating the state. 208 * @return A View that needs to draw before ending the recents animation to LAST_TASK. 209 * (This is a hack to ensure Taskbar draws its background first to avoid flickering.) 210 */ onSettledOnEndTarget(GestureState.GestureEndTarget endTarget)211 public @Nullable View onSettledOnEndTarget(GestureState.GestureEndTarget endTarget) { 212 TaskbarUIController taskbarUIController = getTaskbarController(); 213 if (taskbarUIController != null) { 214 taskbarUIController.setSystemGestureInProgress(false); 215 return taskbarUIController.getRootView(); 216 } 217 return null; 218 } 219 220 /** 221 * Called when the current gesture transition is cancelled. 222 * @param activityVisible Whether the user can see the changes we make here, so try to animate. 223 * @param endTarget If the gesture ended before we got cancelled, where we were headed. 224 */ onTransitionCancelled(boolean activityVisible, @Nullable GestureState.GestureEndTarget endTarget)225 public void onTransitionCancelled(boolean activityVisible, 226 @Nullable GestureState.GestureEndTarget endTarget) { 227 RecentsViewContainer container = getCreatedContainer(); 228 if (container == null) { 229 return; 230 } 231 RecentsView recentsView = container.getOverviewPanel(); 232 BaseState startState = recentsView.getStateManager().getRestState(); 233 if (endTarget != null) { 234 // We were on our way to this state when we got canceled, end there instead. 235 startState = stateFromGestureEndTarget(endTarget); 236 final var context = recentsView.getContext(); 237 if (DesktopVisibilityController.INSTANCE.get(context) 238 .isInDesktopModeAndNotInOverview(context.getDisplayId()) 239 && endTarget == LAST_TASK) { 240 // When we are cancelling the transition and going back to last task, move to 241 // rest state instead when desktop tasks are visible. 242 // If a fullscreen task is visible, launcher goes to normal state when the 243 // activity is stopped. This does not happen when desktop tasks are visible 244 // on top of launcher. Force the launcher state to rest state here. 245 startState = recentsView.getStateManager().getRestState(); 246 // Do not animate the transition 247 activityVisible = false; 248 } 249 } 250 recentsView.getStateManager().goToState(startState, activityVisible); 251 } 252 calculateTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)253 public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect, 254 RecentsPagedOrientationHandler orientationHandler) { 255 if (dp.isTablet) { 256 calculateLargeTileSize(context, dp, outRect); 257 } else { 258 Resources res = context.getResources(); 259 float maxScale = res.getFloat(R.dimen.overview_max_scale); 260 int taskMargin = dp.overviewTaskMarginPx; 261 // In fake orientation, OverviewActions is hidden and we only leave a margin there. 262 int overviewActionsClaimedSpace = orientationHandler.isLayoutNaturalToLauncher() 263 ? dp.getOverviewActionsClaimedSpace() : dp.overviewActionsTopMarginPx; 264 calculateTaskSizeInternal( 265 context, 266 dp, 267 dp.overviewTaskThumbnailTopMarginPx, 268 overviewActionsClaimedSpace, 269 res.getDimensionPixelSize(R.dimen.overview_minimum_next_prev_size) + taskMargin, 270 maxScale, 271 Gravity.CENTER, 272 outRect, 273 orientationHandler); 274 } 275 } 276 calculateLargeTileSize(Context context, DeviceProfile dp, Rect outRect)277 private void calculateLargeTileSize(Context context, DeviceProfile dp, Rect outRect) { 278 Resources res = context.getResources(); 279 float maxScale = res.getFloat(R.dimen.overview_max_scale); 280 Rect gridRect = new Rect(); 281 calculateGridSize(dp, context, gridRect); 282 calculateTaskSizeInternal(context, dp, gridRect, maxScale, Gravity.CENTER, outRect); 283 } 284 calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove, int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity, Rect outRect, RecentsPagedOrientationHandler orientationHandler)285 private void calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove, 286 int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity, 287 Rect outRect, RecentsPagedOrientationHandler orientationHandler) { 288 Rect potentialTaskRect = new Rect(0, 0, dp.widthPx, dp.heightPx); 289 290 Rect insets; 291 if (orientationHandler.isLayoutNaturalToLauncher()) { 292 insets = dp.getInsets(); 293 } else { 294 Rect portraitInsets = dp.getInsets(); 295 DisplayController displayController = DisplayController.INSTANCE.get(context); 296 @Nullable List<WindowBounds> windowBounds = 297 displayController.getInfo().getCurrentBounds(); 298 Rect deviceRotationInsets = windowBounds != null 299 ? windowBounds.get(orientationHandler.getRotation()).insets 300 : new Rect(); 301 // Obtain the landscape/seascape insets, and rotate it to portrait perspective. 302 orientationHandler.rotateInsets(deviceRotationInsets, outRect); 303 // Then combine with portrait's insets to leave space for status bar/nav bar in 304 // either orientations. 305 outRect.set( 306 Math.max(outRect.left, portraitInsets.left), 307 Math.max(outRect.top, portraitInsets.top), 308 Math.max(outRect.right, portraitInsets.right), 309 Math.max(outRect.bottom, portraitInsets.bottom) 310 ); 311 insets = outRect; 312 } 313 potentialTaskRect.inset(insets); 314 315 outRect.set( 316 minimumHorizontalPadding, 317 claimedSpaceAbove, 318 minimumHorizontalPadding, 319 claimedSpaceBelow); 320 // Rotate the paddings to portrait perspective, 321 orientationHandler.rotateInsets(outRect, outRect); 322 potentialTaskRect.inset(outRect); 323 324 calculateTaskSizeInternal(context, dp, potentialTaskRect, maxScale, gravity, outRect); 325 } 326 calculateTaskSizeInternal(Context context, DeviceProfile dp, Rect potentialTaskRect, float targetScale, int gravity, Rect outRect)327 private void calculateTaskSizeInternal(Context context, DeviceProfile dp, 328 Rect potentialTaskRect, float targetScale, int gravity, Rect outRect) { 329 PointF taskDimension = getTaskDimension(context, dp); 330 331 float scale = Math.min( 332 potentialTaskRect.width() / taskDimension.x, 333 potentialTaskRect.height() / taskDimension.y); 334 scale = Math.min(scale, targetScale); 335 int outWidth = Math.round(scale * taskDimension.x); 336 int outHeight = Math.round(scale * taskDimension.y); 337 338 Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect); 339 } 340 getTaskDimension(Context context, DeviceProfile dp)341 private static PointF getTaskDimension(Context context, DeviceProfile dp) { 342 PointF dimension = new PointF(); 343 getTaskDimension(context, dp, dimension); 344 return dimension; 345 } 346 347 /** 348 * Gets the dimension of the task in the current system state. 349 */ getTaskDimension(Context context, DeviceProfile dp, PointF out)350 public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) { 351 out.x = dp.widthPx; 352 out.y = dp.heightPx; 353 if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) { 354 out.y -= dp.taskbarHeight; 355 } 356 } 357 358 /** 359 * Calculates the overview grid size for the provided device configuration. 360 */ calculateGridSize(DeviceProfile dp, Context context, Rect outRect)361 public final void calculateGridSize(DeviceProfile dp, Context context, Rect outRect) { 362 Rect insets = dp.getInsets(); 363 int topMargin = dp.overviewTaskThumbnailTopMarginPx; 364 int bottomMargin = dp.getOverviewActionsClaimedSpace(); 365 int sideMargin = dp.overviewGridSideMargin; 366 367 outRect.set(0, 0, dp.widthPx, dp.heightPx); 368 outRect.inset(Math.max(insets.left, sideMargin), insets.top + topMargin, 369 Math.max(insets.right, sideMargin), Math.max(insets.bottom, bottomMargin)); 370 } 371 372 /** 373 * Calculates the overview grid non-focused task size for the provided device configuration. 374 */ calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)375 public final void calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect, 376 RecentsPagedOrientationHandler orientationHandler) { 377 Resources res = context.getResources(); 378 Rect potentialTaskRect = new Rect(); 379 calculateLargeTileSize(context, dp, potentialTaskRect); 380 381 float rowHeight = (potentialTaskRect.height() + dp.overviewTaskThumbnailTopMarginPx 382 - dp.overviewRowSpacing) / 2f; 383 384 PointF taskDimension = getTaskDimension(context, dp); 385 float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / taskDimension.y; 386 int outWidth = Math.round(scale * taskDimension.x); 387 int outHeight = Math.round(scale * taskDimension.y); 388 389 int gravity = Gravity.TOP; 390 gravity |= orientationHandler.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT; 391 Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect); 392 } 393 394 /** 395 * Calculates the modal taskView size for the provided device configuration 396 */ calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)397 public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect, 398 RecentsPagedOrientationHandler orientationHandler) { 399 calculateTaskSize(context, dp, outRect, orientationHandler); 400 boolean isGridOnlyOverview = dp.isTablet && Flags.enableGridOnlyOverview(); 401 int claimedSpaceBelow = isGridOnlyOverview 402 ? dp.overviewActionsTopMarginPx + dp.overviewActionsHeight + dp.stashedTaskbarHeight 403 : (dp.heightPx - outRect.bottom - dp.getInsets().bottom); 404 int minimumHorizontalPadding = 0; 405 if (!isGridOnlyOverview) { 406 float maxScale = context.getResources().getFloat(R.dimen.overview_modal_max_scale); 407 minimumHorizontalPadding = 408 Math.round((dp.availableWidthPx - outRect.width() * maxScale) / 2); 409 } 410 calculateTaskSizeInternal( 411 context, 412 dp, 413 dp.overviewTaskMarginPx, 414 claimedSpaceBelow, 415 minimumHorizontalPadding, 416 1f /*maxScale*/, 417 Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 418 outRect, 419 orientationHandler); 420 } 421 onInitBackgroundStateUI()422 protected void onInitBackgroundStateUI() { 423 if (mOnInitBackgroundStateUICallback != null) { 424 mOnInitBackgroundStateUICallback.run(); 425 mOnInitBackgroundStateUICallback = null; 426 } 427 } 428 } 429