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