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