1 /* 2 * Copyright (C) 2018 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.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; 19 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; 20 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; 21 import static com.android.launcher3.Utilities.postAsyncCallback; 22 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; 23 import static com.android.launcher3.anim.Interpolators.DEACCEL; 24 import static com.android.launcher3.anim.Interpolators.LINEAR; 25 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; 26 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 27 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; 28 import static com.android.launcher3.util.RaceConditionTracker.ENTER; 29 import static com.android.launcher3.util.RaceConditionTracker.EXIT; 30 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; 31 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; 32 import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE; 33 import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK; 34 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES; 35 import static com.android.quickstep.TouchInteractionService.MAIN_THREAD_EXECUTOR; 36 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; 37 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME; 38 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK; 39 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK; 40 import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS; 41 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; 42 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; 43 44 import android.animation.Animator; 45 import android.animation.AnimatorSet; 46 import android.animation.ObjectAnimator; 47 import android.animation.TimeInterpolator; 48 import android.animation.ValueAnimator; 49 import android.annotation.TargetApi; 50 import android.app.ActivityManager.RunningTaskInfo; 51 import android.content.Context; 52 import android.graphics.Canvas; 53 import android.graphics.Point; 54 import android.graphics.PointF; 55 import android.graphics.Rect; 56 import android.graphics.RectF; 57 import android.os.Build; 58 import android.os.Handler; 59 import android.os.Looper; 60 import android.os.SystemClock; 61 import android.util.Log; 62 import android.view.HapticFeedbackConstants; 63 import android.view.MotionEvent; 64 import android.view.View; 65 import android.view.View.OnApplyWindowInsetsListener; 66 import android.view.ViewTreeObserver.OnDrawListener; 67 import android.view.WindowInsets; 68 import android.view.WindowManager; 69 import android.view.animation.Interpolator; 70 71 import androidx.annotation.NonNull; 72 import androidx.annotation.Nullable; 73 import androidx.annotation.UiThread; 74 75 import com.android.launcher3.AbstractFloatingView; 76 import com.android.launcher3.BaseDraggingActivity; 77 import com.android.launcher3.DeviceProfile; 78 import com.android.launcher3.InvariantDeviceProfile; 79 import com.android.launcher3.R; 80 import com.android.launcher3.Utilities; 81 import com.android.launcher3.anim.AnimationSuccessListener; 82 import com.android.launcher3.anim.AnimatorPlaybackController; 83 import com.android.launcher3.anim.Interpolators; 84 import com.android.launcher3.graphics.RotationMode; 85 import com.android.launcher3.logging.UserEventDispatcher; 86 import com.android.launcher3.testing.TestProtocol; 87 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 88 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 89 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 90 import com.android.launcher3.util.RaceConditionTracker; 91 import com.android.launcher3.util.TraceHelper; 92 import com.android.launcher3.views.FloatingIconView; 93 import com.android.quickstep.ActivityControlHelper.ActivityInitListener; 94 import com.android.quickstep.ActivityControlHelper.AnimationFactory; 95 import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState; 96 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory; 97 import com.android.quickstep.SysUINavigationMode.Mode; 98 import com.android.quickstep.inputconsumers.InputConsumer; 99 import com.android.quickstep.inputconsumers.OverviewInputConsumer; 100 import com.android.quickstep.util.ClipAnimationHelper; 101 import com.android.quickstep.util.RectFSpringAnim; 102 import com.android.quickstep.util.RemoteAnimationTargetSet; 103 import com.android.quickstep.util.SwipeAnimationTargetSet; 104 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener; 105 import com.android.quickstep.views.LiveTileOverlay; 106 import com.android.quickstep.views.RecentsView; 107 import com.android.quickstep.views.TaskView; 108 import com.android.systemui.shared.recents.model.ThumbnailData; 109 import com.android.systemui.shared.system.InputConsumerController; 110 import com.android.systemui.shared.system.LatencyTrackerCompat; 111 import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 112 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; 113 import com.android.systemui.shared.system.WindowCallbacksCompat; 114 115 import java.util.function.BiFunction; 116 import java.util.function.Consumer; 117 118 @TargetApi(Build.VERSION_CODES.O) 119 public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> 120 implements SwipeAnimationListener, OnApplyWindowInsetsListener { 121 private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName(); 122 123 private static final Rect TEMP_RECT = new Rect(); 124 125 private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null; 126 getFlagForIndex(int index, String name)127 private static int getFlagForIndex(int index, String name) { 128 if (DEBUG_STATES) { 129 STATE_NAMES[index] = name; 130 } 131 return 1 << index; 132 } 133 134 // Launcher UI related states 135 private static final int STATE_LAUNCHER_PRESENT = getFlagForIndex(0, "STATE_LAUNCHER_PRESENT"); 136 private static final int STATE_LAUNCHER_STARTED = getFlagForIndex(1, "STATE_LAUNCHER_STARTED"); 137 private static final int STATE_LAUNCHER_DRAWN = getFlagForIndex(2, "STATE_LAUNCHER_DRAWN"); 138 139 // Internal initialization states 140 private static final int STATE_APP_CONTROLLER_RECEIVED = 141 getFlagForIndex(3, "STATE_APP_CONTROLLER_RECEIVED"); 142 143 // Interaction finish states 144 private static final int STATE_SCALED_CONTROLLER_HOME = 145 getFlagForIndex(4, "STATE_SCALED_CONTROLLER_HOME"); 146 private static final int STATE_SCALED_CONTROLLER_RECENTS = 147 getFlagForIndex(5, "STATE_SCALED_CONTROLLER_RECENTS"); 148 149 private static final int STATE_HANDLER_INVALIDATED = 150 getFlagForIndex(6, "STATE_HANDLER_INVALIDATED"); 151 private static final int STATE_GESTURE_STARTED = 152 getFlagForIndex(7, "STATE_GESTURE_STARTED"); 153 private static final int STATE_GESTURE_CANCELLED = 154 getFlagForIndex(8, "STATE_GESTURE_CANCELLED"); 155 private static final int STATE_GESTURE_COMPLETED = 156 getFlagForIndex(9, "STATE_GESTURE_COMPLETED"); 157 158 private static final int STATE_CAPTURE_SCREENSHOT = 159 getFlagForIndex(10, "STATE_CAPTURE_SCREENSHOT"); 160 private static final int STATE_SCREENSHOT_CAPTURED = 161 getFlagForIndex(11, "STATE_SCREENSHOT_CAPTURED"); 162 private static final int STATE_SCREENSHOT_VIEW_SHOWN = 163 getFlagForIndex(12, "STATE_SCREENSHOT_VIEW_SHOWN"); 164 165 private static final int STATE_RESUME_LAST_TASK = 166 getFlagForIndex(13, "STATE_RESUME_LAST_TASK"); 167 private static final int STATE_START_NEW_TASK = 168 getFlagForIndex(14, "STATE_START_NEW_TASK"); 169 private static final int STATE_CURRENT_TASK_FINISHED = 170 getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED"); 171 172 private static final int LAUNCHER_UI_STATES = 173 STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED; 174 175 public enum GestureEndTarget { 176 HOME(1, STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT, true, false, 177 ContainerType.WORKSPACE, false), 178 179 RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT 180 | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true), 181 182 NEW_TASK(0, STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT, false, true, 183 ContainerType.APP, true), 184 185 LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false); 186 GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued, int containerType, boolean recentsAttachedToAppWindow)187 GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued, 188 int containerType, boolean recentsAttachedToAppWindow) { 189 this.endShift = endShift; 190 this.endState = endState; 191 this.isLauncher = isLauncher; 192 this.canBeContinued = canBeContinued; 193 this.containerType = containerType; 194 this.recentsAttachedToAppWindow = recentsAttachedToAppWindow; 195 } 196 197 /** 0 is app, 1 is overview */ 198 public final float endShift; 199 /** The state to apply when we reach this final target */ 200 public final int endState; 201 /** Whether the target is in the launcher activity */ 202 public final boolean isLauncher; 203 /** Whether the user can start a new gesture while this one is finishing */ 204 public final boolean canBeContinued; 205 /** Used to log where the user ended up after the gesture ends */ 206 public final int containerType; 207 /** Whether RecentsView should be attached to the window as we animate to this target */ 208 public final boolean recentsAttachedToAppWindow; 209 } 210 211 public static final long MAX_SWIPE_DURATION = 350; 212 public static final long MIN_SWIPE_DURATION = 80; 213 public static final long MIN_OVERSHOOT_DURATION = 120; 214 215 public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f; 216 private static final float SWIPE_DURATION_MULTIPLIER = 217 Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); 218 private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured"; 219 220 private static final long SHELF_ANIM_DURATION = 240; 221 public static final long RECENTS_ATTACH_DURATION = 300; 222 223 // Start resisting when swiping past this factor of mTransitionDragLength. 224 private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f; 225 // This is how far down we can scale down, where 0f is full screen and 1f is recents. 226 private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f; 227 private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL; 228 229 /** 230 * Used as the page index for logging when we return to the last task at the end of the gesture. 231 */ 232 private static final int LOG_NO_OP_PAGE_INDEX = -1; 233 234 private final ClipAnimationHelper mClipAnimationHelper; 235 private final ClipAnimationHelper.TransformParams mTransformParams; 236 237 private Runnable mGestureEndCallback; 238 private GestureEndTarget mGestureEndTarget; 239 // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise 240 private RunningWindowAnim mRunningWindowAnim; 241 private boolean mIsShelfPeeking; 242 private DeviceProfile mDp; 243 // The distance needed to drag to reach the task size in recents. 244 private int mTransitionDragLength; 245 // How much further we can drag past recents, as a factor of mTransitionDragLength. 246 private float mDragLengthFactor = 1; 247 248 // Shift in the range of [0, 1]. 249 // 0 => preview snapShot is completely visible, and hotseat is completely translated down 250 // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely 251 // visible. 252 private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); 253 private boolean mContinuingLastGesture; 254 // To avoid UI jump when gesture is started, we offset the animation by the threshold. 255 private float mShiftAtGestureStart = 0; 256 257 private final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler(); 258 259 private final Context mContext; 260 private final ActivityControlHelper<T> mActivityControlHelper; 261 private final ActivityInitListener mActivityInitListener; 262 263 private final SysUINavigationMode.Mode mMode; 264 265 private final int mRunningTaskId; 266 private ThumbnailData mTaskSnapshot; 267 268 private MultiStateCallback mStateCallback; 269 // Used to control launcher components throughout the swipe gesture. 270 private AnimatorPlaybackController mLauncherTransitionController; 271 private boolean mHasLauncherTransitionControllerStarted; 272 273 private T mActivity; 274 private RecentsView mRecentsView; 275 private AnimationFactory mAnimationFactory = (t) -> { }; 276 private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay(); 277 278 private boolean mCanceled; 279 private boolean mWasLauncherAlreadyVisible; 280 private int mFinishingRecentsAnimationForNewTaskId = -1; 281 282 private boolean mPassedOverviewThreshold; 283 private boolean mGestureStarted; 284 private int mLogAction = Touch.SWIPE; 285 private int mLogDirection = Direction.UP; 286 private PointF mDownPos; 287 private boolean mIsLikelyToStartNewTask; 288 289 private final RecentsAnimationWrapper mRecentsAnimationWrapper; 290 291 private final long mTouchTimeMs; 292 private long mLauncherFrameDrawnTime; 293 WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs, ActivityControlHelper<T> controller, boolean continuingLastGesture, InputConsumerController inputConsumer)294 public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, 295 long touchTimeMs, ActivityControlHelper<T> controller, boolean continuingLastGesture, 296 InputConsumerController inputConsumer) { 297 mContext = context; 298 mRunningTaskId = runningTaskInfo.id; 299 mTouchTimeMs = touchTimeMs; 300 mActivityControlHelper = controller; 301 mActivityInitListener = mActivityControlHelper 302 .createActivityInitListener(this::onActivityInit); 303 mContinuingLastGesture = continuingLastGesture; 304 mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, 305 this::createNewInputProxyHandler); 306 mClipAnimationHelper = new ClipAnimationHelper(context); 307 mTransformParams = new ClipAnimationHelper.TransformParams(); 308 309 mMode = SysUINavigationMode.getMode(context); 310 initStateCallbacks(); 311 312 DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); 313 initTransitionEndpoints(dp); 314 } 315 initStateCallbacks()316 private void initStateCallbacks() { 317 mStateCallback = new MultiStateCallback(STATE_NAMES); 318 319 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, 320 this::onLauncherPresentAndGestureStarted); 321 322 mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED, 323 this::initializeLauncherAnimationController); 324 325 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, 326 this::launcherFrameDrawn); 327 328 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED 329 | STATE_GESTURE_CANCELLED, 330 this::resetStateForAnimationCancel); 331 332 mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, 333 this::sendRemoteAnimationsToAnimationFactory); 334 335 mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, 336 this::resumeLastTask); 337 mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED, 338 this::startNewTask); 339 340 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED 341 | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT, 342 this::switchToScreenshot); 343 344 mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED 345 | STATE_SCALED_CONTROLLER_RECENTS, 346 this::finishCurrentTransitionToRecents); 347 348 mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED 349 | STATE_SCALED_CONTROLLER_HOME, 350 this::finishCurrentTransitionToHome); 351 mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED, 352 this::reset); 353 354 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED 355 | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS 356 | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED 357 | STATE_GESTURE_STARTED, 358 this::setupLauncherUiAfterSwipeUpToRecentsAnimation); 359 360 mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); 361 mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, 362 this::invalidateHandlerWithLauncher); 363 mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK, 364 this::notifyTransitionCancelled); 365 366 mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED, 367 mRecentsAnimationWrapper::enableInputConsumer); 368 369 if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { 370 mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT 371 | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT, 372 (b) -> mRecentsView.setRunningTaskHidden(!b)); 373 } 374 } 375 setStateOnUiThread(int stateFlag)376 private void setStateOnUiThread(int stateFlag) { 377 if (Looper.myLooper() == mMainThreadHandler.getLooper()) { 378 mStateCallback.setState(stateFlag); 379 } else { 380 postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); 381 } 382 } 383 initTransitionEndpoints(DeviceProfile dp)384 private void initTransitionEndpoints(DeviceProfile dp) { 385 mDp = dp; 386 387 Rect tempRect = new Rect(); 388 mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength( 389 dp, mContext, tempRect); 390 mClipAnimationHelper.updateTargetRect(tempRect); 391 if (mMode == Mode.NO_BUTTON) { 392 // We can drag all the way to the top of the screen. 393 mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength; 394 } 395 } 396 getFadeInDuration()397 private long getFadeInDuration() { 398 if (mCurrentShift.getCurrentAnimation() != null) { 399 ObjectAnimator anim = mCurrentShift.getCurrentAnimation(); 400 long theirDuration = anim.getDuration() - anim.getCurrentPlayTime(); 401 402 // TODO: Find a better heuristic 403 return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION)); 404 } else { 405 return MAX_SWIPE_DURATION; 406 } 407 } 408 initWhenReady()409 public void initWhenReady() { 410 mActivityInitListener.register(); 411 } 412 onActivityInit(final T activity, Boolean alreadyOnHome)413 private boolean onActivityInit(final T activity, Boolean alreadyOnHome) { 414 if (mActivity == activity) { 415 return true; 416 } 417 if (mActivity != null) { 418 // The launcher may have been recreated as a result of device rotation. 419 int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; 420 initStateCallbacks(); 421 mStateCallback.setState(oldState); 422 } 423 mWasLauncherAlreadyVisible = alreadyOnHome; 424 mActivity = activity; 425 // Override the visibility of the activity until the gesture actually starts and we swipe 426 // up, or until we transition home and the home animation is composed 427 if (alreadyOnHome) { 428 mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); 429 } else { 430 mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); 431 } 432 433 mRecentsView = activity.getOverviewPanel(); 434 SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> { 435 mTransformParams.setSyncTransactionApplier(applier); 436 mRecentsAnimationWrapper.runOnInit(() -> 437 mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier)); 438 }); 439 440 mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { 441 if (mGestureEndTarget != HOME) { 442 updateFinalShift(); 443 } 444 }); 445 mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper); 446 mRecentsView.setClipAnimationHelper(mClipAnimationHelper); 447 mRecentsView.setLiveTileOverlay(mLiveTileOverlay); 448 mActivity.getRootView().getOverlay().add(mLiveTileOverlay); 449 450 mStateCallback.setState(STATE_LAUNCHER_PRESENT); 451 if (alreadyOnHome) { 452 onLauncherStart(activity); 453 } else { 454 activity.setOnStartCallback(this::onLauncherStart); 455 } 456 457 setupRecentsViewUi(); 458 return true; 459 } 460 onLauncherStart(final T activity)461 private void onLauncherStart(final T activity) { 462 if (TestProtocol.sDebugTracing) { 463 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart"); 464 } 465 if (mActivity != activity) { 466 return; 467 } 468 if (TestProtocol.sDebugTracing) { 469 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 1"); 470 } 471 if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { 472 return; 473 } 474 if (TestProtocol.sDebugTracing) { 475 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 2"); 476 } 477 478 // If we've already ended the gesture and are going home, don't prepare recents UI, 479 // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL. 480 if (mGestureEndTarget != HOME) { 481 if (TestProtocol.sDebugTracing) { 482 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 3"); 483 } 484 Runnable initAnimFactory = () -> { 485 if (TestProtocol.sDebugTracing) { 486 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 4"); 487 } 488 mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, 489 mWasLauncherAlreadyVisible, true, 490 this::onAnimatorPlaybackControllerCreated); 491 maybeUpdateRecentsAttachedState(false /* animate */); 492 }; 493 if (mWasLauncherAlreadyVisible) { 494 // Launcher is visible, but might be about to stop. Thus, if we prepare recents 495 // now, it might get overridden by moveToRestState() in onStop(). To avoid this, 496 // wait until the next gesture (and possibly launcher) starts. 497 if (TestProtocol.sDebugTracing) { 498 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 5"); 499 } 500 mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory); 501 } else { 502 if (TestProtocol.sDebugTracing) { 503 Log.d(TestProtocol.NO_OVERVIEW_EVENT_TAG, "onLauncherStart 6"); 504 } 505 initAnimFactory.run(); 506 } 507 } 508 AbstractFloatingView.closeAllOpenViewsExcept(activity, mWasLauncherAlreadyVisible, 509 AbstractFloatingView.TYPE_LISTENER); 510 511 if (mWasLauncherAlreadyVisible) { 512 mStateCallback.setState(STATE_LAUNCHER_DRAWN); 513 } else { 514 TraceHelper.beginSection("WTS-init"); 515 View dragLayer = activity.getDragLayer(); 516 dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { 517 518 @Override 519 public void onDraw() { 520 TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); 521 dragLayer.post(() -> 522 dragLayer.getViewTreeObserver().removeOnDrawListener(this)); 523 if (activity != mActivity) { 524 return; 525 } 526 527 mStateCallback.setState(STATE_LAUNCHER_DRAWN); 528 } 529 }); 530 } 531 532 activity.getRootView().setOnApplyWindowInsetsListener(this); 533 mStateCallback.setState(STATE_LAUNCHER_STARTED); 534 } 535 onLauncherPresentAndGestureStarted()536 private void onLauncherPresentAndGestureStarted() { 537 // Re-setup the recents UI when gesture starts, as the state could have been changed during 538 // that time by a previous window transition. 539 setupRecentsViewUi(); 540 541 notifyGestureStartedAsync(); 542 } 543 setupRecentsViewUi()544 private void setupRecentsViewUi() { 545 if (mContinuingLastGesture) { 546 updateSysUiFlags(mCurrentShift.value); 547 return; 548 } 549 mRecentsView.onGestureAnimationStart(mRunningTaskId); 550 } 551 launcherFrameDrawn()552 private void launcherFrameDrawn() { 553 mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); 554 } 555 sendRemoteAnimationsToAnimationFactory()556 private void sendRemoteAnimationsToAnimationFactory() { 557 mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); 558 } 559 initializeLauncherAnimationController()560 private void initializeLauncherAnimationController() { 561 buildAnimationController(); 562 563 if (LatencyTrackerCompat.isEnabled(mContext)) { 564 LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); 565 } 566 567 // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the 568 // high-res thumbnail loader here once we are sure that we will end up in an overview state 569 RecentsModel.INSTANCE.get(mContext).getThumbnailCache() 570 .getHighResLoadingState().setVisible(true); 571 } 572 getTaskCurveScaleForOffsetX(float offsetX, float taskWidth)573 private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) { 574 float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + 575 mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing); 576 float interpolation = Math.min(1, offsetX / distanceToReachEdge); 577 return TaskView.getCurveScaleForInterpolation(interpolation); 578 } 579 getRecentsViewDispatcher(RotationMode rotationMode)580 public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) { 581 return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null; 582 } 583 584 @UiThread updateDisplacement(float displacement)585 public void updateDisplacement(float displacement) { 586 // We are moving in the negative x/y direction 587 displacement = -displacement; 588 if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) { 589 mCurrentShift.updateValue(mDragLengthFactor); 590 } else { 591 float translation = Math.max(displacement, 0); 592 float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; 593 if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) { 594 float pullbackProgress = Utilities.getProgress(shift, 595 DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor); 596 pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress); 597 shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress 598 * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK); 599 } 600 mCurrentShift.updateValue(shift); 601 } 602 } 603 onMotionPauseChanged(boolean isPaused)604 public void onMotionPauseChanged(boolean isPaused) { 605 setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION); 606 } 607 maybeUpdateRecentsAttachedState()608 public void maybeUpdateRecentsAttachedState() { 609 maybeUpdateRecentsAttachedState(true /* animate */); 610 } 611 612 /** 613 * Determines whether to show or hide RecentsView. The window is always 614 * synchronized with its corresponding TaskView in RecentsView, so if 615 * RecentsView is shown, it will appear to be attached to the window. 616 * 617 * Note this method has no effect unless the navigation mode is NO_BUTTON. 618 */ maybeUpdateRecentsAttachedState(boolean animate)619 private void maybeUpdateRecentsAttachedState(boolean animate) { 620 if (mMode != Mode.NO_BUTTON || mRecentsView == null) { 621 return; 622 } 623 RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationWrapper.targetSet == null 624 ? null 625 : mRecentsAnimationWrapper.targetSet.findTask(mRunningTaskId); 626 final boolean recentsAttachedToAppWindow; 627 int runningTaskIndex = mRecentsView.getRunningTaskIndex(); 628 if (mGestureEndTarget != null) { 629 recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow; 630 } else if (mContinuingLastGesture 631 && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) { 632 recentsAttachedToAppWindow = true; 633 animate = false; 634 } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) { 635 // The window is going away so make sure recents is always visible in this case. 636 recentsAttachedToAppWindow = true; 637 animate = false; 638 } else { 639 recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask; 640 if (animate) { 641 // Only animate if an adjacent task view is visible on screen. 642 TaskView adjacentTask1 = mRecentsView.getTaskViewAt(runningTaskIndex + 1); 643 TaskView adjacentTask2 = mRecentsView.getTaskViewAt(runningTaskIndex - 1); 644 float prevTranslationX = mRecentsView.getTranslationX(); 645 mRecentsView.setTranslationX(0); 646 animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT)) 647 || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT)); 648 mRecentsView.setTranslationX(prevTranslationX); 649 } 650 } 651 mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate); 652 } 653 setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask)654 public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { 655 if (mIsLikelyToStartNewTask != isLikelyToStartNewTask) { 656 mIsLikelyToStartNewTask = isLikelyToStartNewTask; 657 maybeUpdateRecentsAttachedState(); 658 } 659 } 660 661 @UiThread setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration)662 public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) { 663 mAnimationFactory.setShelfState(shelfState, interpolator, duration); 664 boolean wasShelfPeeking = mIsShelfPeeking; 665 mIsShelfPeeking = shelfState == PEEK; 666 if (mIsShelfPeeking != wasShelfPeeking) { 667 maybeUpdateRecentsAttachedState(); 668 } 669 if (mRecentsView != null && shelfState.shouldPreformHaptic) { 670 mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 671 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 672 } 673 } 674 buildAnimationController()675 private void buildAnimationController() { 676 if (mGestureEndTarget == HOME || mHasLauncherTransitionControllerStarted) { 677 // We don't want a new mLauncherTransitionController if mGestureEndTarget == HOME (it 678 // has its own animation) or if we're already animating the current controller. 679 return; 680 } 681 initTransitionEndpoints(mActivity.getDeviceProfile()); 682 mAnimationFactory.createActivityController(mTransitionDragLength); 683 } 684 685 @Override onApplyWindowInsets(View view, WindowInsets windowInsets)686 public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { 687 WindowInsets result = view.onApplyWindowInsets(windowInsets); 688 buildAnimationController(); 689 return result; 690 } 691 onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim)692 private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { 693 mLauncherTransitionController = anim; 694 mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor); 695 mAnimationFactory.adjustActivityControllerInterpolators(); 696 mLauncherTransitionController.dispatchOnStart(); 697 updateLauncherTransitionProgress(); 698 } 699 700 @UiThread updateFinalShift()701 private void updateFinalShift() { 702 float shift = mCurrentShift.value; 703 704 SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController(); 705 if (controller != null) { 706 float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset(); 707 float offsetScale = getTaskCurveScaleForOffsetX(offsetX, 708 mClipAnimationHelper.getTargetRect().width()); 709 mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale); 710 mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, 711 mTransformParams); 712 updateSysUiFlags(shift); 713 } 714 715 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 716 if (mRecentsAnimationWrapper.getController() != null) { 717 mLiveTileOverlay.update(mClipAnimationHelper.getCurrentRectWithInsets(), 718 mClipAnimationHelper.getCurrentCornerRadius()); 719 } 720 } 721 722 final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; 723 if (passed != mPassedOverviewThreshold) { 724 mPassedOverviewThreshold = passed; 725 if (mRecentsView != null && mMode != Mode.NO_BUTTON) { 726 mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 727 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 728 } 729 } 730 731 if (mLauncherTransitionController == null || mLauncherTransitionController 732 .getAnimationPlayer().isStarted()) { 733 return; 734 } 735 updateLauncherTransitionProgress(); 736 } 737 updateLauncherTransitionProgress()738 private void updateLauncherTransitionProgress() { 739 if (mGestureEndTarget == HOME) { 740 return; 741 } 742 // Normalize the progress to 0 to 1, as the animation controller will clamp it to that 743 // anyway. The controller mimics the drag length factor by applying it to its interpolators. 744 float progress = mCurrentShift.value / mDragLengthFactor; 745 mLauncherTransitionController.setPlayFraction( 746 progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1 747 ? 0 : (progress - mShiftAtGestureStart) / (1 - mShiftAtGestureStart)); 748 } 749 750 /** 751 * @param windowProgress 0 == app, 1 == overview 752 */ updateSysUiFlags(float windowProgress)753 private void updateSysUiFlags(float windowProgress) { 754 if (mRecentsView != null) { 755 TaskView centermostTask = mRecentsView.getTaskViewAt(mRecentsView 756 .getPageNearestToCenterOfScreen()); 757 int centermostTaskFlags = centermostTask == null ? 0 758 : centermostTask.getThumbnail().getSysUiStatusNavFlags(); 759 boolean useHomeScreenFlags = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; 760 // We will handle the sysui flags based on the centermost task view. 761 mRecentsAnimationWrapper.setWindowThresholdCrossed(centermostTaskFlags != 0 762 || useHomeScreenFlags); 763 int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags; 764 mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags); 765 } 766 } 767 768 @Override onRecentsAnimationStart(SwipeAnimationTargetSet targetSet)769 public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) { 770 DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); 771 final Rect overviewStackBounds; 772 RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); 773 774 if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) { 775 overviewStackBounds = mActivityControlHelper 776 .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget); 777 dp = dp.getMultiWindowProfile(mContext, new Point( 778 targetSet.minimizedHomeBounds.width(), targetSet.minimizedHomeBounds.height())); 779 dp.updateInsets(targetSet.homeContentInsets); 780 } else { 781 if (mActivity != null) { 782 int loc[] = new int[2]; 783 View rootView = mActivity.getRootView(); 784 rootView.getLocationOnScreen(loc); 785 overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), 786 loc[1] + rootView.getHeight()); 787 } else { 788 overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); 789 } 790 // If we are not in multi-window mode, home insets should be same as system insets. 791 dp = dp.copy(mContext); 792 dp.updateInsets(targetSet.homeContentInsets); 793 } 794 dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); 795 796 if (runningTaskTarget != null) { 797 mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); 798 } 799 mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */); 800 initTransitionEndpoints(dp); 801 802 mRecentsAnimationWrapper.setController(targetSet); 803 TOUCH_INTERACTION_LOG.addLog("startRecentsAnimationCallback", targetSet.apps.length); 804 setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); 805 806 mPassedOverviewThreshold = false; 807 } 808 809 @Override onRecentsAnimationCanceled()810 public void onRecentsAnimationCanceled() { 811 mRecentsAnimationWrapper.setController(null); 812 mActivityInitListener.unregister(); 813 setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); 814 TOUCH_INTERACTION_LOG.addLog("cancelRecentsAnimation"); 815 } 816 817 @UiThread onGestureStarted()818 public void onGestureStarted() { 819 notifyGestureStartedAsync(); 820 mShiftAtGestureStart = mCurrentShift.value; 821 setStateOnUiThread(STATE_GESTURE_STARTED); 822 mGestureStarted = true; 823 } 824 825 /** 826 * Notifies the launcher that the swipe gesture has started. This can be called multiple times. 827 */ 828 @UiThread notifyGestureStartedAsync()829 private void notifyGestureStartedAsync() { 830 final T curActivity = mActivity; 831 if (curActivity != null) { 832 // Once the gesture starts, we can no longer transition home through the button, so 833 // reset the force override of the activity visibility 834 mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); 835 } 836 } 837 838 /** 839 * Called as a result on ACTION_CANCEL to return the UI to the start state. 840 */ 841 @UiThread onGestureCancelled()842 public void onGestureCancelled() { 843 updateDisplacement(0); 844 setStateOnUiThread(STATE_GESTURE_COMPLETED); 845 mLogAction = Touch.SWIPE_NOOP; 846 handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */); 847 } 848 849 /** 850 * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen. 851 * @param velocity The x and y components of the velocity when the gesture ends. 852 * @param downPos The x and y value of where the gesture started. 853 */ 854 @UiThread onGestureEnded(float endVelocity, PointF velocity, PointF downPos)855 public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) { 856 float flingThreshold = mContext.getResources() 857 .getDimension(R.dimen.quickstep_fling_threshold_velocity); 858 boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; 859 setStateOnUiThread(STATE_GESTURE_COMPLETED); 860 861 mLogAction = isFling ? Touch.FLING : Touch.SWIPE; 862 boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x); 863 if (isVelocityVertical) { 864 mLogDirection = velocity.y < 0 ? Direction.UP : Direction.DOWN; 865 } else { 866 mLogDirection = velocity.x < 0 ? Direction.LEFT : Direction.RIGHT; 867 } 868 mDownPos = downPos; 869 handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */); 870 } 871 872 @UiThread 873 private InputConsumer createNewInputProxyHandler() { 874 endRunningWindowAnim(); 875 endLauncherTransitionController(); 876 if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { 877 // Hide the task view, if not already hidden 878 setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); 879 } 880 881 BaseDraggingActivity activity = mActivityControlHelper.getCreatedActivity(); 882 return activity == null 883 ? InputConsumer.NO_OP : new OverviewInputConsumer(activity, null, true); 884 } 885 886 private void endRunningWindowAnim() { 887 if (mRunningWindowAnim != null) { 888 mRunningWindowAnim.end(); 889 } 890 } 891 892 private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling, 893 boolean isCancel) { 894 final GestureEndTarget endTarget; 895 final boolean goingToNewTask; 896 if (mRecentsView != null) { 897 if (!mRecentsAnimationWrapper.hasTargets()) { 898 // If there are no running tasks, then we can assume that this is a continuation of 899 // the last gesture, but after the recents animation has finished 900 goingToNewTask = true; 901 } else { 902 final int runningTaskIndex = mRecentsView.getRunningTaskIndex(); 903 final int taskToLaunch = mRecentsView.getNextPage(); 904 goingToNewTask = runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex; 905 } 906 } else { 907 goingToNewTask = false; 908 } 909 final boolean reachedOverviewThreshold = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; 910 if (!isFling) { 911 if (isCancel) { 912 endTarget = LAST_TASK; 913 } else if (mMode == Mode.NO_BUTTON) { 914 if (mIsShelfPeeking) { 915 endTarget = RECENTS; 916 } else if (goingToNewTask) { 917 endTarget = NEW_TASK; 918 } else { 919 endTarget = !reachedOverviewThreshold ? LAST_TASK : HOME; 920 } 921 } else { 922 endTarget = reachedOverviewThreshold && mGestureStarted 923 ? RECENTS 924 : goingToNewTask 925 ? NEW_TASK 926 : LAST_TASK; 927 } 928 } else { 929 if (mMode == Mode.NO_BUTTON && endVelocity < 0 && !mIsShelfPeeking) { 930 // If swiping at a diagonal, base end target on the faster velocity. 931 endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity) 932 ? NEW_TASK : HOME; 933 } else if (endVelocity < 0) { 934 if (reachedOverviewThreshold) { 935 endTarget = RECENTS; 936 } else { 937 // If swiping at a diagonal, base end target on the faster velocity. 938 endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity) 939 ? NEW_TASK : RECENTS; 940 } 941 } else { 942 endTarget = goingToNewTask ? NEW_TASK : LAST_TASK; 943 } 944 } 945 946 int stateFlags = OverviewInteractionState.INSTANCE.get(mActivity).getSystemUiStateFlags(); 947 if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0 948 && (endTarget == RECENTS || endTarget == LAST_TASK)) { 949 return LAST_TASK; 950 } 951 return endTarget; 952 } 953 954 @UiThread handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity, boolean isCancel)955 private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity, 956 boolean isCancel) { 957 PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000); 958 long duration = MAX_SWIPE_DURATION; 959 float currentShift = mCurrentShift.value; 960 final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity, 961 isFling, isCancel); 962 float endShift = endTarget.endShift; 963 final float startShift; 964 Interpolator interpolator = DEACCEL; 965 if (!isFling) { 966 long expectedDuration = Math.abs(Math.round((endShift - currentShift) 967 * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); 968 duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); 969 startShift = currentShift; 970 interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL; 971 } else { 972 startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y 973 * SINGLE_FRAME_MS / mTransitionDragLength, 0, mDragLengthFactor); 974 float minFlingVelocity = mContext.getResources() 975 .getDimension(R.dimen.quickstep_fling_min_velocity); 976 if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { 977 if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) { 978 Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( 979 startShift, endShift, endShift, velocityPxPerMs.y, 980 mTransitionDragLength); 981 endShift = overshoot.end; 982 interpolator = overshoot.interpolator; 983 duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, 984 MAX_SWIPE_DURATION); 985 } else { 986 float distanceToTravel = (endShift - currentShift) * mTransitionDragLength; 987 988 // we want the page's snap velocity to approximately match the velocity at 989 // which the user flings, so we scale the duration by a value near to the 990 // derivative of the scroll interpolator at zero, ie. 2. 991 long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y)); 992 duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); 993 994 if (endTarget == RECENTS) { 995 interpolator = OVERSHOOT_1_2; 996 } 997 } 998 } 999 } 1000 1001 if (endTarget.isLauncher) { 1002 mRecentsAnimationWrapper.enableInputProxy(); 1003 } 1004 1005 if (endTarget == HOME) { 1006 setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); 1007 duration = Math.max(MIN_OVERSHOOT_DURATION, duration); 1008 } else if (endTarget == RECENTS) { 1009 mLiveTileOverlay.startIconAnimation(); 1010 if (mRecentsView != null) { 1011 int nearestPage = mRecentsView.getPageNearestToCenterOfScreen(); 1012 if (mRecentsView.getNextPage() != nearestPage) { 1013 // We shouldn't really scroll to the next page when swiping up to recents. 1014 // Only allow settling on the next page if it's nearest to the center. 1015 mRecentsView.snapToPage(nearestPage, Math.toIntExact(duration)); 1016 } 1017 if (mRecentsView.getScroller().getDuration() > MAX_SWIPE_DURATION) { 1018 mRecentsView.snapToPage(mRecentsView.getNextPage(), (int) MAX_SWIPE_DURATION); 1019 } 1020 duration = Math.max(duration, mRecentsView.getScroller().getDuration()); 1021 } 1022 if (mMode == Mode.NO_BUTTON) { 1023 setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration); 1024 } 1025 } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) { 1026 // Let RecentsView handle the scrolling to the task, which we launch in startNewTask() 1027 // or resumeLastTask(). 1028 if (mRecentsView != null) { 1029 duration = Math.max(duration, mRecentsView.getScroller().getDuration()); 1030 } 1031 } 1032 animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs); 1033 } 1034 doLogGesture(GestureEndTarget endTarget)1035 private void doLogGesture(GestureEndTarget endTarget) { 1036 DeviceProfile dp = mDp; 1037 if (dp == null || mDownPos == null) { 1038 // We probably never received an animation controller, skip logging. 1039 return; 1040 } 1041 1042 int pageIndex = endTarget == LAST_TASK 1043 ? LOG_NO_OP_PAGE_INDEX 1044 : mRecentsView.getNextPage(); 1045 UserEventDispatcher.newInstance(mContext).logStateChangeAction( 1046 mLogAction, mLogDirection, 1047 (int) mDownPos.x, (int) mDownPos.y, 1048 ContainerType.NAVBAR, ContainerType.APP, 1049 endTarget.containerType, 1050 pageIndex); 1051 } 1052 1053 /** Animates to the given progress, where 0 is the current app and 1 is overview. */ 1054 @UiThread animateToProgress(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs)1055 private void animateToProgress(float start, float end, long duration, Interpolator interpolator, 1056 GestureEndTarget target, PointF velocityPxPerMs) { 1057 mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, 1058 interpolator, target, velocityPxPerMs)); 1059 } 1060 1061 @UiThread animateToProgressInternal(float start, float end, long duration, Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs)1062 private void animateToProgressInternal(float start, float end, long duration, 1063 Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { 1064 mGestureEndTarget = target; 1065 1066 maybeUpdateRecentsAttachedState(); 1067 1068 if (mGestureEndTarget == HOME) { 1069 HomeAnimationFactory homeAnimFactory; 1070 if (mActivity != null) { 1071 homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity); 1072 } else { 1073 homeAnimFactory = new HomeAnimationFactory() { 1074 @NonNull 1075 @Override 1076 public RectF getWindowTargetRect() { 1077 RectF fallbackTarget = new RectF(mClipAnimationHelper.getTargetRect()); 1078 Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f); 1079 return fallbackTarget; 1080 } 1081 1082 @NonNull 1083 @Override 1084 public AnimatorPlaybackController createActivityAnimationToHome() { 1085 return AnimatorPlaybackController.wrap(new AnimatorSet(), duration); 1086 } 1087 }; 1088 mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, 1089 isPresent -> mRecentsView.startHome()); 1090 } 1091 RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory); 1092 windowAnim.addAnimatorListener(new AnimationSuccessListener() { 1093 @Override 1094 public void onAnimationSuccess(Animator animator) { 1095 setStateOnUiThread(target.endState); 1096 } 1097 }); 1098 windowAnim.start(velocityPxPerMs); 1099 homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y); 1100 mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); 1101 mLauncherTransitionController = null; 1102 } else { 1103 ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end); 1104 windowAnim.setDuration(duration).setInterpolator(interpolator); 1105 windowAnim.addUpdateListener(valueAnimator -> { 1106 if (mRecentsView != null && mRecentsView.getVisibility() != View.VISIBLE) { 1107 // Views typically don't compute scroll when invisible as an optimization, 1108 // but in our case we need to since the window offset depends on the scroll. 1109 mRecentsView.computeScroll(); 1110 } 1111 }); 1112 windowAnim.addListener(new AnimationSuccessListener() { 1113 @Override 1114 public void onAnimationSuccess(Animator animator) { 1115 setStateOnUiThread(target.endState); 1116 } 1117 }); 1118 windowAnim.start(); 1119 mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim); 1120 } 1121 // Always play the entire launcher animation when going home, since it is separate from 1122 // the animation that has been controlled thus far. 1123 if (mGestureEndTarget == HOME) { 1124 start = 0; 1125 } 1126 1127 // We want to use the same interpolator as the window, but need to adjust it to 1128 // interpolate over the remaining progress (end - start). 1129 TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress( 1130 interpolator, start, end); 1131 if (mLauncherTransitionController == null) { 1132 return; 1133 } 1134 if (start == end || duration <= 0) { 1135 mLauncherTransitionController.dispatchSetInterpolator(t -> end); 1136 } else { 1137 mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator); 1138 mAnimationFactory.adjustActivityControllerInterpolators(); 1139 } 1140 mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration)); 1141 1142 if (QUICKSTEP_SPRINGS.get()) { 1143 mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y); 1144 } 1145 mLauncherTransitionController.getAnimationPlayer().start(); 1146 mHasLauncherTransitionControllerStarted = true; 1147 } 1148 1149 /** 1150 * Creates an animation that transforms the current app window into the home app. 1151 * @param startProgress The progress of {@link #mCurrentShift} to start the window from. 1152 * @param homeAnimationFactory The home animation factory. 1153 */ createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)1154 private RectFSpringAnim createWindowAnimationToHome(float startProgress, 1155 HomeAnimationFactory homeAnimationFactory) { 1156 final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; 1157 final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet, 1158 mTransformParams.setProgress(startProgress), false /* launcherOnTop */)); 1159 final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); 1160 1161 final View floatingView = homeAnimationFactory.getFloatingView(); 1162 final boolean isFloatingIconView = floatingView instanceof FloatingIconView; 1163 RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mActivity.getResources()); 1164 if (isFloatingIconView) { 1165 FloatingIconView fiv = (FloatingIconView) floatingView; 1166 anim.addAnimatorListener(fiv); 1167 fiv.setOnTargetChangeListener(anim::onTargetPositionChanged); 1168 } 1169 1170 AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome(); 1171 1172 // End on a "round-enough" radius so that the shape reveal doesn't have to do too much 1173 // rounding at the end of the animation. 1174 float startRadius = mClipAnimationHelper.getCurrentCornerRadius(); 1175 float endRadius = startRect.width() / 6f; 1176 // We want the window alpha to be 0 once this threshold is met, so that the 1177 // FolderIconView can be seen morphing into the icon shape. 1178 final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f; 1179 anim.addOnUpdateListener((currentRect, progress) -> { 1180 homeAnim.setPlayFraction(progress); 1181 1182 float alphaProgress = ACCEL_1_5.getInterpolation(progress); 1183 float windowAlpha = Utilities.boundToRange(Utilities.mapToRange(alphaProgress, 0, 1184 windowAlphaThreshold, 1.5f, 0f, Interpolators.LINEAR), 0, 1); 1185 mTransformParams.setProgress(progress) 1186 .setCurrentRectAndTargetAlpha(currentRect, windowAlpha); 1187 if (isFloatingIconView) { 1188 mTransformParams.setCornerRadius(endRadius * progress + startRadius 1189 * (1f - progress)); 1190 } 1191 mClipAnimationHelper.applyTransform(targetSet, mTransformParams, 1192 false /* launcherOnTop */); 1193 1194 if (isFloatingIconView) { 1195 ((FloatingIconView) floatingView).update(currentRect, 1f, progress, 1196 windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false); 1197 } 1198 1199 updateSysUiFlags(Math.max(progress, mCurrentShift.value)); 1200 }); 1201 anim.addAnimatorListener(new AnimationSuccessListener() { 1202 @Override 1203 public void onAnimationStart(Animator animation) { 1204 homeAnim.dispatchOnStart(); 1205 if (mActivity != null) { 1206 mActivity.getRootView().getOverlay().remove(mLiveTileOverlay); 1207 } 1208 } 1209 1210 @Override 1211 public void onAnimationSuccess(Animator animator) { 1212 homeAnim.getAnimationPlayer().end(); 1213 if (mRecentsView != null) { 1214 mRecentsView.post(mRecentsView::resetTaskVisuals); 1215 } 1216 // Make sure recents is in its final state 1217 maybeUpdateRecentsAttachedState(false); 1218 mActivityControlHelper.onSwipeUpToHomeComplete(mActivity); 1219 } 1220 }); 1221 return anim; 1222 } 1223 1224 /** 1225 * @return The GestureEndTarget if the gesture has ended, else null. 1226 */ getGestureEndTarget()1227 public @Nullable GestureEndTarget getGestureEndTarget() { 1228 return mGestureEndTarget; 1229 } 1230 1231 @UiThread resumeLastTask()1232 private void resumeLastTask() { 1233 mRecentsAnimationWrapper.finish(false /* toRecents */, null); 1234 TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", false); 1235 doLogGesture(LAST_TASK); 1236 reset(); 1237 } 1238 1239 @UiThread startNewTask()1240 private void startNewTask() { 1241 // Launch the task user scrolled to (mRecentsView.getNextPage()). 1242 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 1243 // We finish recents animation inside launchTask() when live tile is enabled. 1244 mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false /* animate */, 1245 true /* freezeTaskList */); 1246 } else { 1247 int taskId = mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).getTask().key.id; 1248 mFinishingRecentsAnimationForNewTaskId = taskId; 1249 mRecentsAnimationWrapper.finish(true /* toRecents */, () -> { 1250 if (!mCanceled) { 1251 TaskView nextTask = mRecentsView.getTaskView(taskId); 1252 if (nextTask != null) { 1253 nextTask.launchTask(false /* animate */, true /* freezeTaskList */, 1254 success -> { 1255 if (!success) { 1256 // We couldn't launch the task, so take user to overview so they can 1257 // decide what to do instead of staying in this broken state. 1258 endLauncherTransitionController(); 1259 mActivityControlHelper.onLaunchTaskFailed(mActivity); 1260 nextTask.notifyTaskLaunchFailed(TAG); 1261 updateSysUiFlags(1 /* windowProgress == overview */); 1262 } else { 1263 mActivityControlHelper.onLaunchTaskSuccess(mActivity); 1264 } 1265 }, mMainThreadHandler); 1266 doLogGesture(NEW_TASK); 1267 } 1268 reset(); 1269 } 1270 mCanceled = false; 1271 mFinishingRecentsAnimationForNewTaskId = -1; 1272 }); 1273 } 1274 TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); 1275 } 1276 reset()1277 public void reset() { 1278 setStateOnUiThread(STATE_HANDLER_INVALIDATED); 1279 } 1280 1281 /** 1282 * Cancels any running animation so that the active target can be overriden by a new swipe 1283 * handle (in case of quick switch). 1284 */ cancelCurrentAnimation(SwipeSharedState sharedState)1285 public void cancelCurrentAnimation(SwipeSharedState sharedState) { 1286 mCanceled = true; 1287 mCurrentShift.cancelAnimation(); 1288 if (mLauncherTransitionController != null && mLauncherTransitionController 1289 .getAnimationPlayer().isStarted()) { 1290 mLauncherTransitionController.getAnimationPlayer().cancel(); 1291 } 1292 1293 if (mFinishingRecentsAnimationForNewTaskId != -1) { 1294 // If we are canceling mid-starting a new task, switch to the screenshot since the 1295 // recents animation has finished 1296 switchToScreenshot(); 1297 TaskView newRunningTaskView = mRecentsView.getTaskView( 1298 mFinishingRecentsAnimationForNewTaskId); 1299 int newRunningTaskId = newRunningTaskView != null 1300 ? newRunningTaskView.getTask().key.id 1301 : -1; 1302 mRecentsView.setCurrentTask(newRunningTaskId); 1303 sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId); 1304 } 1305 } 1306 invalidateHandler()1307 private void invalidateHandler() { 1308 endRunningWindowAnim(); 1309 1310 if (mGestureEndCallback != null) { 1311 mGestureEndCallback.run(); 1312 } 1313 1314 mActivityInitListener.unregister(); 1315 mTaskSnapshot = null; 1316 } 1317 invalidateHandlerWithLauncher()1318 private void invalidateHandlerWithLauncher() { 1319 endLauncherTransitionController(); 1320 1321 mRecentsView.onGestureAnimationEnd(); 1322 1323 mActivity.getRootView().setOnApplyWindowInsetsListener(null); 1324 mActivity.getRootView().getOverlay().remove(mLiveTileOverlay); 1325 } 1326 endLauncherTransitionController()1327 private void endLauncherTransitionController() { 1328 setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); 1329 if (mLauncherTransitionController != null) { 1330 mLauncherTransitionController.getAnimationPlayer().end(); 1331 mLauncherTransitionController = null; 1332 } 1333 } 1334 notifyTransitionCancelled()1335 private void notifyTransitionCancelled() { 1336 mAnimationFactory.onTransitionCancelled(); 1337 } 1338 resetStateForAnimationCancel()1339 private void resetStateForAnimationCancel() { 1340 boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; 1341 mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); 1342 1343 // Leave the pending invisible flag, as it may be used by wallpaper open animation. 1344 mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); 1345 } 1346 switchToScreenshot()1347 private void switchToScreenshot() { 1348 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 1349 setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); 1350 } else if (!mRecentsAnimationWrapper.hasTargets()) { 1351 // If there are no targets, then we don't need to capture anything 1352 setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); 1353 } else { 1354 boolean finishTransitionPosted = false; 1355 SwipeAnimationTargetSet controller = mRecentsAnimationWrapper.getController(); 1356 if (controller != null) { 1357 // Update the screenshot of the task 1358 if (mTaskSnapshot == null) { 1359 mTaskSnapshot = controller.screenshotTask(mRunningTaskId); 1360 } 1361 final TaskView taskView; 1362 if (mGestureEndTarget == HOME) { 1363 // Capture the screenshot before finishing the transition to home to ensure it's 1364 // taken in the correct orientation, but no need to update the thumbnail. 1365 taskView = null; 1366 } else { 1367 taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); 1368 } 1369 if (taskView != null && !mCanceled) { 1370 // Defer finishing the animation until the next launcher frame with the 1371 // new thumbnail 1372 finishTransitionPosted = new WindowCallbacksCompat(taskView) { 1373 1374 // The number of frames to defer until we actually finish the animation 1375 private int mDeferFrameCount = 2; 1376 1377 @Override 1378 public void onPostDraw(Canvas canvas) { 1379 // If we were cancelled after this was attached, do not update 1380 // the state. 1381 if (mCanceled) { 1382 detach(); 1383 return; 1384 } 1385 1386 if (mDeferFrameCount > 0) { 1387 mDeferFrameCount--; 1388 // Workaround, detach and reattach to invalidate the root node for 1389 // another draw 1390 detach(); 1391 attach(); 1392 taskView.invalidate(); 1393 return; 1394 } 1395 1396 setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); 1397 detach(); 1398 } 1399 }.attach(); 1400 } 1401 } 1402 if (!finishTransitionPosted) { 1403 // If we haven't posted a draw callback, set the state immediately. 1404 RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, ENTER); 1405 setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); 1406 RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, EXIT); 1407 } 1408 } 1409 } 1410 finishCurrentTransitionToRecents()1411 private void finishCurrentTransitionToRecents() { 1412 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 1413 setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); 1414 } else if (!mRecentsAnimationWrapper.hasTargets()) { 1415 // If there are no targets, then there is nothing to finish 1416 setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); 1417 } else { 1418 synchronized (mRecentsAnimationWrapper) { 1419 mRecentsAnimationWrapper.finish(true /* toRecents */, 1420 () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); 1421 } 1422 } 1423 TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); 1424 } 1425 finishCurrentTransitionToHome()1426 private void finishCurrentTransitionToHome() { 1427 synchronized (mRecentsAnimationWrapper) { 1428 mRecentsAnimationWrapper.finish(true /* toRecents */, 1429 () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED), 1430 true /* sendUserLeaveHint */); 1431 } 1432 TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); 1433 doLogGesture(HOME); 1434 } 1435 setupLauncherUiAfterSwipeUpToRecentsAnimation()1436 private void setupLauncherUiAfterSwipeUpToRecentsAnimation() { 1437 endLauncherTransitionController(); 1438 mActivityControlHelper.onSwipeUpToRecentsComplete(mActivity); 1439 mRecentsAnimationWrapper.setCancelWithDeferredScreenshot(true); 1440 mRecentsView.onSwipeUpAnimationSuccess(); 1441 1442 RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); 1443 1444 doLogGesture(RECENTS); 1445 reset(); 1446 } 1447 setGestureEndCallback(Runnable gestureEndCallback)1448 public void setGestureEndCallback(Runnable gestureEndCallback) { 1449 mGestureEndCallback = gestureEndCallback; 1450 } 1451 setTargetAlphaProvider( BiFunction<RemoteAnimationTargetCompat, Float, Float> provider)1452 private void setTargetAlphaProvider( 1453 BiFunction<RemoteAnimationTargetCompat, Float, Float> provider) { 1454 mClipAnimationHelper.setTaskAlphaCallback(provider); 1455 updateFinalShift(); 1456 } 1457 getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha)1458 public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) { 1459 if (!isNotInRecents(app)) { 1460 return 0; 1461 } 1462 return expectedAlpha; 1463 } 1464 isNotInRecents(RemoteAnimationTargetCompat app)1465 private static boolean isNotInRecents(RemoteAnimationTargetCompat app) { 1466 return app.isNotInRecents 1467 || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; 1468 } 1469 1470 private interface RunningWindowAnim { 1471 void end(); 1472 wrap(Animator animator)1473 static RunningWindowAnim wrap(Animator animator) { 1474 return animator::end; 1475 } 1476 wrap(RectFSpringAnim rectFSpringAnim)1477 static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) { 1478 return rectFSpringAnim::end; 1479 } 1480 } 1481 } 1482