Home | History | Annotate | Download | only in quickstep
      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 
     17 package com.android.quickstep;
     18 
     19 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
     20 
     21 import android.util.Log;
     22 import android.view.HapticFeedbackConstants;
     23 import android.view.animation.Interpolator;
     24 
     25 import com.android.launcher3.Alarm;
     26 import com.android.launcher3.BaseActivity;
     27 import com.android.launcher3.OnAlarmListener;
     28 import com.android.launcher3.Utilities;
     29 import com.android.launcher3.userevent.nano.LauncherLogProto;
     30 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
     31 import com.android.quickstep.views.RecentsView;
     32 import 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  */
     40 public 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