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.launcher3.uioverrides.touchcontrollers;
17 
18 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
19 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
20 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
21 import static com.android.launcher3.LauncherState.ALL_APPS;
22 import static com.android.launcher3.LauncherState.NORMAL;
23 import static com.android.launcher3.LauncherState.OVERVIEW;
24 import static com.android.launcher3.anim.Interpolators.ACCEL;
25 import static com.android.launcher3.anim.Interpolators.DEACCEL;
26 import static com.android.launcher3.anim.Interpolators.LINEAR;
27 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
28 import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
29 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
30 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
31 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
32 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
33 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
34 
35 import android.animation.TimeInterpolator;
36 import android.animation.ValueAnimator;
37 import android.util.Log;
38 import android.view.MotionEvent;
39 import android.view.animation.Interpolator;
40 
41 import com.android.launcher3.DeviceProfile;
42 import com.android.launcher3.Launcher;
43 import com.android.launcher3.LauncherState;
44 import com.android.launcher3.allapps.AllAppsTransitionController;
45 import com.android.launcher3.anim.Interpolators;
46 import com.android.launcher3.states.StateAnimationConfig;
47 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
48 import com.android.launcher3.testing.TestProtocol;
49 import com.android.launcher3.touch.AbstractStateChangeTouchController;
50 import com.android.launcher3.touch.SingleAxisSwipeDetector;
51 import com.android.launcher3.uioverrides.states.OverviewState;
52 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
53 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
54 import com.android.quickstep.SystemUiProxy;
55 import com.android.quickstep.TouchInteractionService;
56 import com.android.quickstep.util.LayoutUtils;
57 import com.android.quickstep.views.RecentsView;
58 
59 /**
60  * Touch controller for handling various state transitions in portrait UI.
61  */
62 public class PortraitStatesTouchController extends AbstractStateChangeTouchController {
63 
64     private static final String TAG = "PortraitStatesTouchCtrl";
65 
66     /**
67      * The progress at which all apps content will be fully visible when swiping up from overview.
68      */
69     protected static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f;
70 
71     /**
72      * The progress at which recents will begin fading out when swiping up from overview.
73      */
74     private static final float RECENTS_FADE_THRESHOLD = 0.88f;
75 
76     private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper;
77 
78     private final InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper();
79 
80     private final boolean mAllowDragToOverview;
81 
82     // If true, we will finish the current animation instantly on second touch.
83     private boolean mFinishFastOnSecondTouch;
84 
PortraitStatesTouchController(Launcher l, boolean allowDragToOverview)85     public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
86         super(l, SingleAxisSwipeDetector.VERTICAL);
87         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
88         mAllowDragToOverview = allowDragToOverview;
89     }
90 
91     @Override
canInterceptTouch(MotionEvent ev)92     protected boolean canInterceptTouch(MotionEvent ev) {
93         if (mCurrentAnimation != null) {
94             if (mFinishFastOnSecondTouch) {
95                 mCurrentAnimation.getAnimationPlayer().end();
96             }
97 
98             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
99             if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()) {
100                 // If we are already animating from a previous state, we can intercept as long as
101                 // the touch is below the current all apps progress (to allow for double swipe).
102                 return true;
103             }
104             // Otherwise, make sure everything is settled and don't intercept so they can scroll
105             // recents, dismiss a task, etc.
106             if (mAtomicAnim != null) {
107                 mAtomicAnim.end();
108             }
109             return false;
110         }
111         if (mLauncher.isInState(ALL_APPS)) {
112             // In all-apps only listen if the container cannot scroll itself
113             if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
114                 return false;
115             }
116         } else if (mLauncher.isInState(OVERVIEW)) {
117             if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) {
118                 return false;
119             }
120         } else {
121             // If we are swiping to all apps instead of overview, allow it from anywhere.
122             boolean interceptAnywhere = mLauncher.isInState(NORMAL) && !mAllowDragToOverview;
123             // For all other states, only listen if the event originated below the hotseat height
124             if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
125                 return false;
126             }
127         }
128         if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
129             return false;
130         }
131         return true;
132     }
133 
134     @Override
getTargetState(LauncherState fromState, boolean isDragTowardPositive)135     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
136         if (TestProtocol.sDebugTracing) {
137             Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState");
138         }
139         if (fromState == ALL_APPS && !isDragTowardPositive) {
140             // Should swipe down go to OVERVIEW instead?
141             if (TestProtocol.sDebugTracing) {
142                 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
143                         "PortraitStatesTouchController.getTargetState 1");
144             }
145             return TouchInteractionService.isConnected() ?
146                     mLauncher.getStateManager().getLastState() : NORMAL;
147         } else if (fromState == OVERVIEW) {
148             if (TestProtocol.sDebugTracing) {
149                 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
150                         "PortraitStatesTouchController.getTargetState 2");
151             }
152             LauncherState positiveDragTarget = ALL_APPS;
153             if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) {
154                 // Don't allow swiping up to all apps.
155                 positiveDragTarget = OVERVIEW;
156             }
157             return isDragTowardPositive ? positiveDragTarget : NORMAL;
158         } else if (fromState == NORMAL && isDragTowardPositive) {
159             if (TestProtocol.sDebugTracing) {
160                 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
161                         "PortraitStatesTouchController.getTargetState 3");
162             }
163             int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
164             return mAllowDragToOverview && TouchInteractionService.isConnected()
165                     && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0
166                     ? OVERVIEW : ALL_APPS;
167         }
168         return fromState;
169     }
170 
171     @Override
getLogContainerTypeForNormalState(MotionEvent ev)172     protected int getLogContainerTypeForNormalState(MotionEvent ev) {
173         return isTouchOverHotseat(mLauncher, ev) ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
174     }
175 
getNormalToOverviewAnimation()176     private StateAnimationConfig getNormalToOverviewAnimation() {
177         mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR;
178 
179         StateAnimationConfig builder = new StateAnimationConfig();
180         builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper);
181         return builder;
182     }
183 
getOverviewToAllAppsAnimation()184     private static StateAnimationConfig getOverviewToAllAppsAnimation() {
185         StateAnimationConfig builder = new StateAnimationConfig();
186         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
187                 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
188         builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL,
189                 RECENTS_FADE_THRESHOLD, 1));
190         return builder;
191     }
192 
getAllAppsToOverviewAnimation()193     private StateAnimationConfig getAllAppsToOverviewAnimation() {
194         StateAnimationConfig builder = new StateAnimationConfig();
195         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
196                 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
197         builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL,
198                 0f, 1 - RECENTS_FADE_THRESHOLD));
199         return builder;
200     }
201 
getNormalToAllAppsAnimation()202     private StateAnimationConfig getNormalToAllAppsAnimation() {
203         StateAnimationConfig builder = new StateAnimationConfig();
204         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL,
205                 0, ALL_APPS_CONTENT_FADE_THRESHOLD));
206         return builder;
207     }
208 
getAllAppsToNormalAnimation()209     private StateAnimationConfig getAllAppsToNormalAnimation() {
210         StateAnimationConfig builder = new StateAnimationConfig();
211         builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL,
212                 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1));
213         return builder;
214     }
215 
216     @Override
getConfigForStates( LauncherState fromState, LauncherState toState)217     protected StateAnimationConfig getConfigForStates(
218             LauncherState fromState, LauncherState toState) {
219         final StateAnimationConfig config;
220         if (fromState == NORMAL && toState == OVERVIEW) {
221             config = getNormalToOverviewAnimation();
222         } else if (fromState == OVERVIEW && toState == ALL_APPS) {
223             config = getOverviewToAllAppsAnimation();
224         } else if (fromState == ALL_APPS && toState == OVERVIEW) {
225             config = getAllAppsToOverviewAnimation();
226         } else if (fromState == NORMAL && toState == ALL_APPS) {
227             config = getNormalToAllAppsAnimation();
228         } else if (fromState == ALL_APPS && toState == NORMAL) {
229             config = getAllAppsToNormalAnimation();
230         }  else {
231             config = new StateAnimationConfig();
232         }
233         return config;
234     }
235 
236     @Override
initCurrentAnimation(@nimationFlags int animFlags)237     protected float initCurrentAnimation(@AnimationFlags int animFlags) {
238         float range = getShiftRange();
239         long maxAccuracy = (long) (2 * range);
240 
241         float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
242         float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
243 
244         float totalShift = endVerticalShift - startVerticalShift;
245 
246         final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig()
247                 : getConfigForStates(mFromState, mToState);
248         config.animFlags = updateAnimComponentsOnReinit(animFlags);
249         config.duration = maxAccuracy;
250 
251         cancelPendingAnim();
252 
253         if (mFromState == OVERVIEW && mToState == NORMAL
254                 && mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) {
255             // Reset the state manager, when changing the interaction mode
256             mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */);
257             mPendingAnimation = mOverviewPortraitStateTouchHelper
258                     .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR);
259             Runnable onCancelRunnable = () -> {
260                 cancelPendingAnim();
261                 clearState();
262             };
263             mCurrentAnimation = mPendingAnimation.createPlaybackController()
264                     .setOnCancelRunnable(onCancelRunnable);
265             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
266             RecentsView recentsView = mLauncher.getOverviewPanel();
267             totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
268                     mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
269         } else {
270             mCurrentAnimation = mLauncher.getStateManager()
271                     .createAnimationToNewWorkspace(mToState, config)
272                     .setOnCancelRunnable(this::clearState);
273         }
274 
275         if (totalShift == 0) {
276             totalShift = Math.signum(mFromState.ordinal - mToState.ordinal)
277                     * OverviewState.getDefaultSwipeHeight(mLauncher);
278         }
279         return 1 / totalShift;
280     }
281 
282     /**
283      * Give subclasses the chance to update the animation when we re-initialize towards a new state.
284      */
285     @AnimationFlags
updateAnimComponentsOnReinit(@nimationFlags int animComponents)286     protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) {
287         return animComponents;
288     }
289 
cancelPendingAnim()290     private void cancelPendingAnim() {
291         if (mPendingAnimation != null) {
292             mPendingAnimation.finish(false, Touch.SWIPE);
293             mPendingAnimation = null;
294         }
295     }
296 
297     @Override
updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)298     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
299             LauncherState targetState, float velocity, boolean isFling) {
300         super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState,
301                 velocity, isFling);
302         handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling);
303     }
304 
handleFirstSwipeToOverview(final ValueAnimator animator, final long expectedDuration, final LauncherState targetState, final float velocity, final boolean isFling)305     private void handleFirstSwipeToOverview(final ValueAnimator animator,
306             final long expectedDuration, final LauncherState targetState, final float velocity,
307             final boolean isFling) {
308         if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
309                 && targetState == OVERVIEW) {
310             mFinishFastOnSecondTouch = true;
311         } else  if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
312             mFinishFastOnSecondTouch = true;
313             if (isFling && expectedDuration != 0) {
314                 // Update all apps interpolator to add a bit of overshoot starting from currFraction
315                 final float currFraction = mCurrentAnimation.getProgressFraction();
316                 mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress(
317                         Interpolators.overshootInterpolatorForVelocity(velocity), currFraction, 1);
318                 animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION))
319                         .setInterpolator(LINEAR);
320             }
321         } else {
322             mFinishFastOnSecondTouch = false;
323         }
324     }
325 
326     @Override
onSwipeInteractionCompleted(LauncherState targetState, int logAction)327     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
328         super.onSwipeInteractionCompleted(targetState, logAction);
329         if (mStartState == NORMAL && targetState == OVERVIEW) {
330             SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
331         }
332     }
333 
334     /**
335      * Whether the motion event is over the hotseat.
336      *
337      * @param launcher the launcher activity
338      * @param ev the event to check
339      * @return true if the event is over the hotseat
340      */
isTouchOverHotseat(Launcher launcher, MotionEvent ev)341     static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) {
342         return (ev.getY() >= getHotseatTop(launcher));
343     }
344 
getHotseatTop(Launcher launcher)345     public static int getHotseatTop(Launcher launcher) {
346         DeviceProfile dp = launcher.getDeviceProfile();
347         int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
348         return launcher.getDragLayer().getHeight() - hotseatHeight;
349     }
350 
351     private static class InterpolatorWrapper implements Interpolator {
352 
353         public TimeInterpolator baseInterpolator = LINEAR;
354 
355         @Override
getInterpolation(float v)356         public float getInterpolation(float v) {
357             return baseInterpolator.getInterpolation(v);
358         }
359     }
360 }
361