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