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