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