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 17package com.android.quickstep; 18 19import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 20 21import android.util.Log; 22import android.view.HapticFeedbackConstants; 23import android.view.animation.Interpolator; 24 25import com.android.launcher3.Alarm; 26import com.android.launcher3.BaseActivity; 27import com.android.launcher3.OnAlarmListener; 28import com.android.launcher3.Utilities; 29import com.android.launcher3.userevent.nano.LauncherLogProto; 30import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 31import com.android.quickstep.views.RecentsView; 32import com.android.quickstep.views.TaskView; 33 34/** 35 * Responds to quick scrub callbacks to page through and launch recent tasks. 36 * 37 * The behavior is to evenly divide the progress into sections, each of which scrolls one page. 38 * The first and last section set an alarm to auto-advance backwards or forwards, respectively. 39 */ 40public class QuickScrubController implements OnAlarmListener { 41 42 public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; 43 public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 200; 44 // We want the translation y to finish faster than the rest of the animation. 45 public static final float QUICK_SCRUB_TRANSLATION_Y_FACTOR = 5f / 6; 46 public static final Interpolator QUICK_SCRUB_START_INTERPOLATOR = FAST_OUT_SLOW_IN; 47 48 /** 49 * Snap to a new page when crossing these thresholds. The first and last auto-advance. 50 */ 51 private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] { 52 0.05f, 0.20f, 0.35f, 0.50f, 0.65f, 0.80f, 0.95f 53 }; 54 55 private static final String TAG = "QuickScrubController"; 56 private static final boolean ENABLE_AUTO_ADVANCE = true; 57 private static final long AUTO_ADVANCE_DELAY = 500; 58 private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325; 59 private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60; 60 61 private final Alarm mAutoAdvanceAlarm; 62 private final RecentsView mRecentsView; 63 private final BaseActivity mActivity; 64 65 private boolean mInQuickScrub; 66 private boolean mWaitingForTaskLaunch; 67 private int mQuickScrubSection; 68 private boolean mStartedFromHome; 69 private boolean mFinishedTransitionToQuickScrub; 70 private Runnable mOnFinishedTransitionToQuickScrubRunnable; 71 private ActivityControlHelper mActivityControlHelper; 72 73 public QuickScrubController(BaseActivity activity, RecentsView recentsView) { 74 mActivity = activity; 75 mRecentsView = recentsView; 76 if (ENABLE_AUTO_ADVANCE) { 77 mAutoAdvanceAlarm = new Alarm(); 78 mAutoAdvanceAlarm.setOnAlarmListener(this); 79 } 80 } 81 82 public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) { 83 prepareQuickScrub(TAG); 84 mInQuickScrub = true; 85 mStartedFromHome = startingFromHome; 86 mQuickScrubSection = 0; 87 mFinishedTransitionToQuickScrub = false; 88 mActivityControlHelper = controlHelper; 89 90 snapToNextTaskIfAvailable(); 91 mActivity.getUserEventDispatcher().resetActionDurationMillis(); 92 } 93 94 public void onQuickScrubEnd() { 95 mInQuickScrub = false; 96 if (ENABLE_AUTO_ADVANCE) { 97 mAutoAdvanceAlarm.cancelAlarm(); 98 } 99 int page = mRecentsView.getNextPage(); 100 Runnable launchTaskRunnable = () -> { 101 TaskView taskView = mRecentsView.getPageAt(page); 102 if (taskView != null) { 103 mWaitingForTaskLaunch = true; 104 taskView.launchTask(true, (result) -> { 105 if (!result) { 106 taskView.notifyTaskLaunchFailed(TAG); 107 breakOutOfQuickScrub(); 108 } else { 109 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(Touch.DRAGDROP, 110 LauncherLogProto.Action.Direction.NONE, page, 111 TaskUtils.getComponentKeyForTask(taskView.getTask().key)); 112 } 113 mWaitingForTaskLaunch = false; 114 }, taskView.getHandler()); 115 } else { 116 breakOutOfQuickScrub(); 117 } 118 mActivityControlHelper = null; 119 }; 120 int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) 121 * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; 122 if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { 123 // Settle on the page then launch it 124 mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable); 125 } else { 126 // No page move needed, just launch it 127 if (mFinishedTransitionToQuickScrub) { 128 launchTaskRunnable.run(); 129 } else { 130 mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable; 131 } 132 } 133 } 134 135 public void cancelActiveQuickscrub() { 136 if (!mInQuickScrub) { 137 return; 138 } 139 Log.d(TAG, "Quickscrub was active, cancelling"); 140 mInQuickScrub = false; 141 mActivityControlHelper = null; 142 mOnFinishedTransitionToQuickScrubRunnable = null; 143 mRecentsView.setNextPageSwitchRunnable(null); 144 } 145 146 /** 147 * Initializes the UI for quick scrub, returns true if success. 148 */ 149 public boolean prepareQuickScrub(String tag) { 150 if (mWaitingForTaskLaunch || mInQuickScrub) { 151 Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); 152 return false; 153 } 154 mOnFinishedTransitionToQuickScrubRunnable = null; 155 mRecentsView.setNextPageSwitchRunnable(null); 156 return true; 157 } 158 159 public boolean isWaitingForTaskLaunch() { 160 return mWaitingForTaskLaunch; 161 } 162 163 /** 164 * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction. 165 */ 166 private void breakOutOfQuickScrub() { 167 if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null 168 || !mActivityControlHelper.switchToRecentsIfVisible(false)) { 169 mActivity.onBackPressed(); 170 } 171 } 172 173 public void onQuickScrubProgress(float progress) { 174 int quickScrubSection = 0; 175 for (float threshold : QUICK_SCRUB_THRESHOLDS) { 176 if (progress < threshold) { 177 break; 178 } 179 quickScrubSection++; 180 } 181 if (quickScrubSection != mQuickScrubSection) { 182 boolean cameFromAutoAdvance = mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length 183 || mQuickScrubSection == 0; 184 int pageToGoTo = mRecentsView.getNextPage() + quickScrubSection - mQuickScrubSection; 185 if (mFinishedTransitionToQuickScrub && !cameFromAutoAdvance) { 186 goToPageWithHaptic(pageToGoTo); 187 } 188 if (ENABLE_AUTO_ADVANCE) { 189 if (quickScrubSection == QUICK_SCRUB_THRESHOLDS.length || quickScrubSection == 0) { 190 mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); 191 } else { 192 mAutoAdvanceAlarm.cancelAlarm(); 193 } 194 } 195 mQuickScrubSection = quickScrubSection; 196 } 197 } 198 199 public void onFinishedTransitionToQuickScrub() { 200 mFinishedTransitionToQuickScrub = true; 201 Runnable action = mOnFinishedTransitionToQuickScrubRunnable; 202 // Clear the runnable before executing it, to prevent potential recursion. 203 mOnFinishedTransitionToQuickScrubRunnable = null; 204 if (action != null) { 205 action.run(); 206 } 207 } 208 209 public void snapToNextTaskIfAvailable() { 210 if (mInQuickScrub && mRecentsView.getChildCount() > 0) { 211 int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION 212 : QUICK_SCRUB_FROM_APP_START_DURATION; 213 int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; 214 goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, 215 QUICK_SCRUB_START_INTERPOLATOR); 216 } 217 } 218 219 private void goToPageWithHaptic(int pageToGoTo) { 220 goToPageWithHaptic(pageToGoTo, -1 /* overrideDuration */, false /* forceHaptic */, null); 221 } 222 223 private void goToPageWithHaptic(int pageToGoTo, int overrideDuration, boolean forceHaptic, 224 Interpolator interpolator) { 225 pageToGoTo = Utilities.boundToRange(pageToGoTo, 0, mRecentsView.getPageCount() - 1); 226 boolean snappingToPage = pageToGoTo != mRecentsView.getNextPage(); 227 if (snappingToPage) { 228 int duration = overrideDuration > -1 ? overrideDuration 229 : Math.abs(pageToGoTo - mRecentsView.getNextPage()) 230 * QUICKSCRUB_SNAP_DURATION_PER_PAGE; 231 mRecentsView.snapToPage(pageToGoTo, duration, interpolator); 232 } 233 if (snappingToPage || forceHaptic) { 234 mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 235 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 236 } 237 } 238 239 @Override 240 public void onAlarm(Alarm alarm) { 241 int currPage = mRecentsView.getNextPage(); 242 boolean recentsVisible = mActivityControlHelper != null 243 && mActivityControlHelper.getVisibleRecentsView() != null; 244 if (!recentsVisible) { 245 Log.w(TAG, "Failed to auto advance; recents not visible"); 246 return; 247 } 248 if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length 249 && currPage < mRecentsView.getPageCount() - 1) { 250 goToPageWithHaptic(currPage + 1); 251 } else if (mQuickScrubSection == 0 && currPage > 0) { 252 goToPageWithHaptic(currPage - 1); 253 } 254 if (ENABLE_AUTO_ADVANCE) { 255 mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY); 256 } 257 } 258} 259