• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.view.Display.DEFAULT_DISPLAY;
19 import static android.view.Surface.ROTATION_0;
20 
21 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
22 import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll;
23 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
24 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
25 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
26 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
27 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
28 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
29 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
30 
31 import android.content.Context;
32 import android.content.res.Resources;
33 import android.view.MotionEvent;
34 import android.view.OrientationEventListener;
35 
36 import com.android.launcher3.dagger.ApplicationContext;
37 import com.android.launcher3.dagger.LauncherAppComponent;
38 import com.android.launcher3.dagger.LauncherAppSingleton;
39 import com.android.launcher3.testing.shared.TestProtocol;
40 import com.android.launcher3.util.DaggerSingletonObject;
41 import com.android.launcher3.util.DaggerSingletonTracker;
42 import com.android.launcher3.util.DisplayController;
43 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
44 import com.android.launcher3.util.DisplayController.Info;
45 import com.android.launcher3.util.NavigationMode;
46 import com.android.quickstep.util.RecentsOrientedState;
47 import com.android.systemui.shared.Flags;
48 import com.android.systemui.shared.system.QuickStepContract;
49 import com.android.systemui.shared.system.TaskStackChangeListener;
50 import com.android.systemui.shared.system.TaskStackChangeListeners;
51 
52 import java.io.PrintWriter;
53 
54 import javax.inject.Inject;
55 
56 /**
57  * Helper class for transforming touch events
58  */
59 @LauncherAppSingleton
60 public class RotationTouchHelper implements DisplayInfoChangeListener {
61 
62     public static final DaggerSingletonObject<RotationTouchHelper> INSTANCE =
63             new DaggerSingletonObject<>(LauncherAppComponent::getRotationTouchHelper);
64 
65     private final OrientationTouchTransformer mOrientationTouchTransformer;
66     private final DisplayController mDisplayController;
67     private final SystemUiProxy mSystemUiProxy;
68     private final int mDisplayId;
69     private int mDisplayRotation;
70 
71     private NavigationMode mMode = THREE_BUTTONS;
72 
73     private final TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
74         @Override
75         public void onRecentTaskListFrozenChanged(boolean frozen) {
76             mTaskListFrozen = frozen;
77             if (frozen || mInOverview) {
78                 return;
79             }
80             enableMultipleRegions(false);
81         }
82 
83         @Override
84         public void onActivityRotation(int displayId) {
85             // This always gets called before onDisplayInfoChanged() so we know how to process
86             // the rotation in that method. This is done to avoid having a race condition between
87             // the sensor readings and onDisplayInfoChanged() call
88             if (displayId != mDisplayId) {
89                 return;
90             }
91 
92             mPrioritizeDeviceRotation = true;
93             if (mInOverview) {
94                 // reset, launcher must be rotating
95                 mExitOverviewRunnable.run();
96             }
97         }
98     };
99 
100     private final Runnable mExitOverviewRunnable = new Runnable() {
101         @Override
102         public void run() {
103             mInOverview = false;
104             enableMultipleRegions(false);
105         }
106     };
107 
108     /**
109      * Used to listen for when the device rotates into the orientation of the current foreground
110      * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then
111      * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
112      * the navbar.
113      */
114     private final OrientationEventListener mOrientationListener;
115     private int mSensorRotation = ROTATION_0;
116     /**
117      * This is the configuration of the foreground app or the app that will be in the foreground
118      * once a quickstep gesture finishes.
119      */
120     private int mCurrentAppRotation = -1;
121     /**
122      * This flag is set to true when the device physically changes orientations. When true, we will
123      * always report the current rotation of the foreground app whenever the display changes, as it
124      * would indicate the user's intention to rotate the foreground app.
125      */
126     private boolean mPrioritizeDeviceRotation = false;
127     /**
128      * Set to true when user swipes to recents. In recents, we ignore the state of the recents
129      * task list being frozen or not to allow the user to keep interacting with nav bar rotation
130      * they went into recents with as opposed to defaulting to the default display rotation.
131      * TODO: (b/156984037) For when user rotates after entering overview
132      */
133     private boolean mInOverview;
134     private boolean mTaskListFrozen;
135     private final Context mContext;
136 
137     @Inject
RotationTouchHelper(@pplicationContext Context context, DisplayController displayController, SystemUiProxy systemUiProxy, DaggerSingletonTracker lifeCycle)138     RotationTouchHelper(@ApplicationContext Context context,
139             DisplayController displayController,
140             SystemUiProxy systemUiProxy,
141             DaggerSingletonTracker lifeCycle) {
142         mContext = context;
143         mDisplayController = displayController;
144         mSystemUiProxy = systemUiProxy;
145         // TODO (b/398195845): this needs updating so non-default displays do not rotate with the
146         //  default display.
147         mDisplayId = DEFAULT_DISPLAY;
148 
149         Resources resources = mContext.getResources();
150         mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
151                 () -> QuickStepContract.getWindowCornerRadius(mContext));
152 
153         // Register for navigation mode and rotation changes
154         mDisplayController.addChangeListenerForDisplay(this, mDisplayId);
155         DisplayController.Info info = mDisplayController.getInfoForDisplay(mDisplayId);
156         onDisplayInfoChanged(context, info, CHANGE_ALL);
157 
158         mOrientationListener = new OrientationEventListener(mContext) {
159             @Override
160             public void onOrientationChanged(int degrees) {
161                 int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
162                         mSensorRotation);
163                 if (newRotation == mSensorRotation) {
164                     return;
165                 }
166 
167                 mSensorRotation = newRotation;
168                 mPrioritizeDeviceRotation = true;
169 
170                 if (newRotation == mCurrentAppRotation) {
171                     // When user rotates device to the orientation of the foreground app after
172                     // quickstepping
173                     toggleSecondaryNavBarsForRotation();
174                 }
175             }
176         };
177 
178         lifeCycle.addCloseable(() -> {
179             mDisplayController.removeChangeListenerForDisplay(this, mDisplayId);
180             mOrientationListener.disable();
181             TaskStackChangeListeners.getInstance()
182                     .unregisterTaskStackListener(mFrozenTaskListener);
183         });
184     }
185 
isTaskListFrozen()186     public boolean isTaskListFrozen() {
187         return mTaskListFrozen;
188     }
189 
touchInAssistantRegion(MotionEvent ev)190     public boolean touchInAssistantRegion(MotionEvent ev) {
191         return mOrientationTouchTransformer.touchInAssistantRegion(ev);
192     }
193 
touchInOneHandedModeRegion(MotionEvent ev)194     public boolean touchInOneHandedModeRegion(MotionEvent ev) {
195         return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev);
196     }
197 
198     /**
199      * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
200      */
updateGestureTouchRegions()201     public void updateGestureTouchRegions() {
202         if (!hasGestures(mMode)) {
203             return;
204         }
205 
206         mOrientationTouchTransformer.createOrAddTouchRegion(
207                 mDisplayController.getInfoForDisplay(mDisplayId),
208                 "RTH.updateGestureTouchRegions");
209     }
210 
211     /**
212      * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
213      */
isInSwipeUpTouchRegion(MotionEvent event)214     public boolean isInSwipeUpTouchRegion(MotionEvent event) {
215         return isInSwipeUpTouchRegion(event, 0);
216     }
217 
218     /**
219      * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
220      *         is in the swipe up gesture region.
221      */
isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex)222     public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
223         if (isTrackpadScroll(event)) {
224             return false;
225         }
226         if (isTrackpadMultiFingerSwipe(event)) {
227             return true;
228         }
229         return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
230                 event.getY(pointerIndex));
231     }
232 
233     @Override
onDisplayInfoChanged(Context context, Info info, int flags)234     public void onDisplayInfoChanged(Context context, Info info, int flags) {
235         if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN | CHANGE_NAVIGATION_MODE
236                 | CHANGE_SUPPORTED_BOUNDS)) != 0) {
237             mDisplayRotation = info.rotation;
238 
239             if (hasGestures(mMode)) {
240                 updateGestureTouchRegions();
241                 mOrientationTouchTransformer.createOrAddTouchRegion(info,
242                         "RTH.onDisplayInfoChanged");
243                 mCurrentAppRotation = mDisplayRotation;
244 
245                 /* Update nav bars on the following:
246                  * a) if this is coming from an activity rotation OR
247                  *   aa) we launch an app in the orientation that user is already in
248                  * b) We're not in overview, since overview will always be portrait (w/o home
249                  *   rotation)
250                  * c) We're actively in quickswitch mode
251                  */
252                 if ((mPrioritizeDeviceRotation
253                         || mCurrentAppRotation == mSensorRotation)
254                         // switch to an app of orientation user is in
255                         && !mInOverview
256                         && mTaskListFrozen) {
257                     toggleSecondaryNavBarsForRotation();
258                 }
259             }
260         }
261 
262         if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
263             NavigationMode newMode = info.getNavigationMode();
264             mOrientationTouchTransformer.setNavigationMode(newMode,
265                     mDisplayController.getInfoForDisplay(mDisplayId),
266                     mContext.getResources());
267 
268             TaskStackChangeListeners.getInstance()
269                     .unregisterTaskStackListener(mFrozenTaskListener);
270             if (hasGestures(newMode)) {
271                 TaskStackChangeListeners.getInstance()
272                         .registerTaskStackListener(mFrozenTaskListener);
273             }
274             mMode = newMode;
275         }
276     }
277 
getDisplayRotation()278     public int getDisplayRotation() {
279         return mDisplayRotation;
280     }
281 
282     /**
283      * Sets the gestural height.
284      */
setGesturalHeight(int newGesturalHeight)285     void setGesturalHeight(int newGesturalHeight) {
286         mOrientationTouchTransformer.setGesturalHeight(
287                 newGesturalHeight, mDisplayController.getInfoForDisplay(mDisplayId),
288                 mContext.getResources());
289     }
290 
291     /**
292      * *May* apply a transform on the motion event if it lies in the nav bar region for another
293      * orientation that is currently being tracked as a part of quickstep
294      */
setOrientationTransformIfNeeded(MotionEvent event)295     void setOrientationTransformIfNeeded(MotionEvent event) {
296         // negative coordinates bug b/143901881
297         if (event.getX() < 0 || event.getY() < 0) {
298             event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
299         }
300         mOrientationTouchTransformer.transform(event);
301     }
302 
enableMultipleRegions(boolean enable)303     private void enableMultipleRegions(boolean enable) {
304         mOrientationTouchTransformer.enableMultipleRegions(enable,
305                 mDisplayController.getInfoForDisplay(mDisplayId));
306         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
307         if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
308             // Clear any previous state from sensor manager
309             mSensorRotation = mCurrentAppRotation;
310             UI_HELPER_EXECUTOR.execute(mOrientationListener::enable);
311         } else {
312             UI_HELPER_EXECUTOR.execute(mOrientationListener::disable);
313         }
314     }
315 
onStartGesture()316     public void onStartGesture() {
317         if (mTaskListFrozen) {
318             // Prioritize whatever nav bar user touches once in quickstep
319             // This case is specifically when user changes what nav bar they are using mid
320             // quickswitch session before tasks list is unfrozen
321             notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
322         }
323     }
324 
onEndTargetCalculated(GestureState.GestureEndTarget endTarget, BaseContainerInterface containerInterface)325     void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
326             BaseContainerInterface containerInterface) {
327         if (endTarget == GestureState.GestureEndTarget.RECENTS) {
328             mInOverview = true;
329             if (!mTaskListFrozen) {
330                 // If we're in landscape w/o ever quickswitching, show the navbar in landscape
331                 enableMultipleRegions(true);
332             }
333             containerInterface.onExitOverview(mExitOverviewRunnable);
334         } else if (endTarget == GestureState.GestureEndTarget.HOME
335                 || endTarget == GestureState.GestureEndTarget.ALL_APPS) {
336             enableMultipleRegions(false);
337         } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
338             if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
339                 // First gesture to start quickswitch
340                 enableMultipleRegions(true);
341             } else {
342                 notifySysuiOfCurrentRotation(
343                         mOrientationTouchTransformer.getCurrentActiveRotation());
344             }
345 
346             // A new gesture is starting, reset the current device rotation
347             // This is done under the assumption that the user won't rotate the phone and then
348             // quickswitch in the old orientation.
349             mPrioritizeDeviceRotation = false;
350         } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
351             if (!mTaskListFrozen) {
352                 // touched nav bar but didn't go anywhere and not quickswitching, do nothing
353                 return;
354             }
355             notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
356         }
357     }
358 
notifySysuiOfCurrentRotation(int rotation)359     private void notifySysuiOfCurrentRotation(int rotation) {
360         UI_HELPER_EXECUTOR.execute(() -> mSystemUiProxy.notifyPrioritizedRotation(rotation));
361     }
362 
363     /**
364      * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
365      * notifies system UI of the primary rotation the user is interacting with
366      */
toggleSecondaryNavBarsForRotation()367     private void toggleSecondaryNavBarsForRotation() {
368         mOrientationTouchTransformer.setSingleActiveRegion(
369                 mDisplayController.getInfoForDisplay(mDisplayId));
370         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
371     }
372 
getCurrentActiveRotation()373     public int getCurrentActiveRotation() {
374         if (!hasGestures(mMode)) {
375             // touch rotation should always match that of display for 3 button
376             return mDisplayRotation;
377         }
378         return mOrientationTouchTransformer.getCurrentActiveRotation();
379     }
380 
dump(PrintWriter pw)381     public void dump(PrintWriter pw) {
382         pw.println("RotationTouchHelper:");
383         pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
384         pw.println("  displayRotation=" + getDisplayRotation());
385         mOrientationTouchTransformer.dump(pw);
386     }
387 
getOrientationTouchTransformer()388     public OrientationTouchTransformer getOrientationTouchTransformer() {
389         return mOrientationTouchTransformer;
390     }
391 
hasGestures(NavigationMode mode)392     private boolean hasGestures(NavigationMode mode) {
393         return mode.hasGestures || (mode == THREE_BUTTONS && Flags.threeButtonCornerSwipe());
394     }
395 }
396