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