1 /*
2  * Copyright (C) 2019 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 
17 package com.android.quickstep;
18 
19 import static android.view.MotionEvent.ACTION_CANCEL;
20 import static android.view.MotionEvent.ACTION_DOWN;
21 import static android.view.MotionEvent.ACTION_MOVE;
22 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
23 import static android.view.MotionEvent.ACTION_UP;
24 
25 import static com.android.launcher3.states.RotationHelper.deltaRotation;
26 
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.graphics.RectF;
30 import android.util.Log;
31 import android.view.MotionEvent;
32 import android.view.Surface;
33 
34 import com.android.launcher3.R;
35 import com.android.launcher3.testing.shared.ResourceUtils;
36 import com.android.launcher3.testing.shared.TestProtocol;
37 import com.android.launcher3.util.DisplayController.Info;
38 import com.android.launcher3.util.NavigationMode;
39 import com.android.launcher3.util.window.CachedDisplayInfo;
40 
41 import java.io.PrintWriter;
42 import java.util.HashMap;
43 import java.util.Map;
44 
45 /**
46  * Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
47  * See {@link OrientationRectF#applyTransformToRotation(MotionEvent, int, boolean)} for
48  * transformation of MotionEvents from one orientation's coordinate space to another's.
49  *
50  * This class only supports single touch/pointer gesture tracking for touches started in a supported
51  * nav bar region.
52  */
53 class OrientationTouchTransformer {
54 
55     private static final String TAG = "OrientationTouchTransformer";
56     private static final boolean DEBUG = false;
57 
58     private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1;
59 
60     private final Map<CachedDisplayInfo, OrientationRectF> mSwipeTouchRegions =
61             new HashMap<CachedDisplayInfo, OrientationRectF>();
62     private final RectF mAssistantLeftRegion = new RectF();
63     private final RectF mAssistantRightRegion = new RectF();
64     private final RectF mOneHandedModeRegion = new RectF();
65     private CachedDisplayInfo mCachedDisplayInfo = new CachedDisplayInfo();
66     private int mNavBarGesturalHeight;
67     private final int mNavBarLargerGesturalHeight;
68     private boolean mEnableMultipleRegions;
69     private Resources mResources;
70     private OrientationRectF mLastRectTouched;
71     /**
72      * The rotation of the last touched nav bar, whether that be through the last region the user
73      * touched down on or valid rotation user turned their device to.
74      * Note this is different than
75      * {@link #mQuickStepStartingRotation} as it always updates its value on every touch whereas
76      * mQuickstepStartingRotation only updates when device rotation matches touch rotation.
77      */
78     private int mActiveTouchRotation;
79     private NavigationMode mMode;
80     private QuickStepContractInfo mContractInfo;
81 
82     /**
83      * Represents if we're currently in a swipe "session" of sorts. If value is
84      * QUICKSTEP_ROTATION_UNINITIALIZED, then user has not tapped on an active nav region.
85      * Otherwise it will be the rotation of the display when the user first interacted with the
86      * active nav bar region.
87      * The "session" ends when {@link #enableMultipleRegions(boolean, Info)} is
88      * called - usually from a timeout or if user starts interacting w/ the foreground app.
89      *
90      * This is different than {@link #mLastRectTouched} as it can get reset by the system whereas
91      * the rect is purely used for tracking touch interactions and usually this "session" will
92      * outlast the touch interaction.
93      */
94     private int mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
95 
96     /** For testability */
97     interface QuickStepContractInfo {
getWindowCornerRadius()98         float getWindowCornerRadius();
99     }
100 
101 
OrientationTouchTransformer(Resources resources, NavigationMode mode, QuickStepContractInfo contractInfo)102     OrientationTouchTransformer(Resources resources, NavigationMode mode,
103             QuickStepContractInfo contractInfo) {
104         mResources = resources;
105         mMode = mode;
106         mContractInfo = contractInfo;
107         mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
108         mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName(
109                 ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, resources,
110                 mNavBarGesturalHeight);
111     }
112 
refreshTouchRegion(Info info, Resources newRes)113     private void refreshTouchRegion(Info info, Resources newRes) {
114         // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
115         // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
116         // It tries to cache and reuse swipe regions whenever possible based only on rotation
117         mResources = newRes;
118         mSwipeTouchRegions.clear();
119         resetSwipeRegions(info);
120     }
121 
setNavigationMode(NavigationMode newMode, Info info, Resources newRes)122     void setNavigationMode(NavigationMode newMode, Info info, Resources newRes) {
123         if (enableLog()) {
124             Log.d(TAG, "setNavigationMode new: " + newMode + " oldMode: " + mMode + " " + this);
125         }
126         if (mMode == newMode) {
127             return;
128         }
129         this.mMode = newMode;
130         refreshTouchRegion(info, newRes);
131     }
132 
setGesturalHeight(int newGesturalHeight, Info info, Resources newRes)133     void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) {
134         if (mNavBarGesturalHeight == newGesturalHeight) {
135             return;
136         }
137         mNavBarGesturalHeight = newGesturalHeight;
138         refreshTouchRegion(info, newRes);
139     }
140 
141     /**
142      * Sets the current nav bar region to listen to events for as determined by
143      * {@param info}. If multiple nav bar regions are enabled, then this region will be added
144      * alongside other regions.
145      * Ok to call multiple times
146      *
147      * @see #enableMultipleRegions(boolean, Info)
148      */
createOrAddTouchRegion(Info info)149     void createOrAddTouchRegion(Info info) {
150         mCachedDisplayInfo = new CachedDisplayInfo(info.currentSize, info.rotation);
151 
152         if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
153                 && mCachedDisplayInfo.rotation == mQuickStepStartingRotation) {
154             // User already was swiping and the current screen is same rotation as the starting one
155             // Remove active nav bars in other rotations except for the one we started out in
156             resetSwipeRegions(info);
157             return;
158         }
159         OrientationRectF region = mSwipeTouchRegions.get(mCachedDisplayInfo);
160         if (region != null) {
161             return;
162         }
163 
164         if (mEnableMultipleRegions) {
165             mSwipeTouchRegions.put(mCachedDisplayInfo, createRegionForDisplay(info));
166         } else {
167             resetSwipeRegions(info);
168         }
169     }
170 
171     /**
172      * Call when we want to start tracking nav bar touch regions in multiple orientations.
173      * ALSO, you BETTER call this with {@param enableMultipleRegions} set to false once you're done.
174      *
175      * @param enableMultipleRegions Set to true to start tracking multiple nav bar regions
176      * @param info The current displayInfo which will be the start of the quickswitch gesture
177      */
enableMultipleRegions(boolean enableMultipleRegions, Info info)178     void enableMultipleRegions(boolean enableMultipleRegions, Info info) {
179         mEnableMultipleRegions = enableMultipleRegions && mMode != NavigationMode.TWO_BUTTONS;
180         if (mEnableMultipleRegions) {
181             mQuickStepStartingRotation = info.rotation;
182         } else {
183             mActiveTouchRotation = 0;
184             mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
185         }
186         resetSwipeRegions(info);
187     }
188 
189     /**
190      * Call when removing multiple regions to swipe from, but still in active quickswitch mode (task
191      * list is still frozen).
192      * Ex. This would be called when user has quickswitched to the same app rotation that
193      * they started quickswitching in, indicating that extra nav regions can be ignored. Calling
194      * this will update the value of {@link #mActiveTouchRotation}
195      *
196      * @param displayInfo The display whos rotation will be used as the current active rotation
197      */
setSingleActiveRegion(Info displayInfo)198     void setSingleActiveRegion(Info displayInfo) {
199         mActiveTouchRotation = displayInfo.rotation;
200         resetSwipeRegions(displayInfo);
201     }
202 
203     /**
204      * Only saves the swipe region represented by {@param region}, clears the
205      * rest from {@link #mSwipeTouchRegions}
206      * To be called whenever we want to stop tracking more than one swipe region.
207      * Ok to call multiple times.
208      */
resetSwipeRegions(Info region)209     private void resetSwipeRegions(Info region) {
210         if (enableLog()) {
211             Log.d(TAG, "clearing all regions except rotation: " + mCachedDisplayInfo.rotation);
212         }
213 
214         mCachedDisplayInfo = new CachedDisplayInfo(region.currentSize, region.rotation);
215         OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCachedDisplayInfo);
216         if (regionToKeep == null) {
217             regionToKeep = createRegionForDisplay(region);
218         }
219         mSwipeTouchRegions.clear();
220         mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
221         updateAssistantRegions(regionToKeep);
222     }
223 
resetSwipeRegions()224     private void resetSwipeRegions() {
225         OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCachedDisplayInfo);
226         mSwipeTouchRegions.clear();
227         if (regionToKeep != null) {
228             mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
229             updateAssistantRegions(regionToKeep);
230         }
231     }
232 
createRegionForDisplay(Info display)233     private OrientationRectF createRegionForDisplay(Info display) {
234         if (enableLog()) {
235             Log.d(TAG, "creating rotation region for: " + mCachedDisplayInfo.rotation
236             + " with mode: " + mMode + " displayRotation: " + display.rotation +
237                     " displaySize: " + display.currentSize +
238                     " navBarHeight: " + mNavBarGesturalHeight);
239         }
240 
241         Point size = display.currentSize;
242         int rotation = display.rotation;
243         int touchHeight = mNavBarGesturalHeight;
244         OrientationRectF orientationRectF = new OrientationRectF(0, 0, size.x, size.y, rotation);
245         if (mMode == NavigationMode.NO_BUTTON) {
246             orientationRectF.top = orientationRectF.bottom - touchHeight;
247             updateAssistantRegions(orientationRectF);
248         } else {
249             mAssistantLeftRegion.setEmpty();
250             mAssistantRightRegion.setEmpty();
251             int navbarSize = getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE);
252             switch (rotation) {
253                 case Surface.ROTATION_90:
254                     orientationRectF.left = orientationRectF.right
255                             - navbarSize;
256                     break;
257                 case Surface.ROTATION_270:
258                     orientationRectF.right = orientationRectF.left
259                             + navbarSize;
260                     break;
261                 default:
262                     orientationRectF.top = orientationRectF.bottom - touchHeight;
263             }
264         }
265         // One handed gestural only active on portrait mode
266         mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
267                 size.x, size.y);
268 
269         return orientationRectF;
270     }
271 
updateAssistantRegions(OrientationRectF orientationRectF)272     private void updateAssistantRegions(OrientationRectF orientationRectF) {
273         int navbarHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE);
274         int assistantWidth = mResources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
275         float assistantHeight = Math.max(navbarHeight, mContractInfo.getWindowCornerRadius());
276         mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = orientationRectF.bottom;
277         mAssistantLeftRegion.top = mAssistantRightRegion.top =
278                 orientationRectF.bottom - assistantHeight;
279 
280         mAssistantLeftRegion.left = 0;
281         mAssistantLeftRegion.right = assistantWidth;
282 
283         mAssistantRightRegion.right = orientationRectF.right;
284         mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
285     }
286 
touchInAssistantRegion(MotionEvent ev)287     boolean touchInAssistantRegion(MotionEvent ev) {
288         return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
289                 || mAssistantRightRegion.contains(ev.getX(), ev.getY());
290 
291     }
292 
touchInOneHandedModeRegion(MotionEvent ev)293     boolean touchInOneHandedModeRegion(MotionEvent ev) {
294         return mOneHandedModeRegion.contains(ev.getX(), ev.getY());
295     }
296 
getNavbarSize(String resName)297     private int getNavbarSize(String resName) {
298         return ResourceUtils.getNavbarSize(resName, mResources);
299     }
300 
touchInValidSwipeRegions(float x, float y)301     boolean touchInValidSwipeRegions(float x, float y) {
302         if (enableLog()) {
303             Log.d(TAG, "touchInValidSwipeRegions " + x + "," + y + " in " + mLastRectTouched);
304         }
305         if (mLastRectTouched != null) {
306             return mLastRectTouched.contains(x, y);
307         }
308         return false;
309     }
310 
getCurrentActiveRotation()311     int getCurrentActiveRotation() {
312         return mActiveTouchRotation;
313     }
314 
getQuickStepStartingRotation()315     int getQuickStepStartingRotation() {
316         return mQuickStepStartingRotation;
317     }
318 
transform(MotionEvent event)319     public void transform(MotionEvent event) {
320         int eventAction = event.getActionMasked();
321         switch (eventAction) {
322             case ACTION_MOVE: {
323                 if (mLastRectTouched == null) {
324                     return;
325                 }
326                 if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) {
327                     if (event.getSurfaceRotation() != mActiveTouchRotation) {
328                         // With Shell transitions, we should rotated to the orientation at the start
329                         // of the gesture not the current display rotation which will happen early
330                         mLastRectTouched.applyTransform(event,
331                                 deltaRotation(event.getSurfaceRotation(), mActiveTouchRotation),
332                                 true);
333                     }
334                 } else {
335                     mLastRectTouched.applyTransformFromRotation(event, mCachedDisplayInfo.rotation,
336                             true);
337                 }
338                 break;
339             }
340             case ACTION_CANCEL:
341             case ACTION_UP: {
342                 if (mLastRectTouched == null) {
343                     return;
344                 }
345                 if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) {
346                     if (event.getSurfaceRotation() != mActiveTouchRotation) {
347                         // With Shell transitions, we should rotated to the orientation at the start
348                         // of the gesture not the current display rotation which will happen early
349                         mLastRectTouched.applyTransform(event,
350                                 deltaRotation(event.getSurfaceRotation(), mActiveTouchRotation),
351                                 true);
352                     }
353                 } else {
354                     mLastRectTouched.applyTransformFromRotation(event, mCachedDisplayInfo.rotation,
355                             true);
356                 }
357                 mLastRectTouched = null;
358                 break;
359             }
360             case ACTION_POINTER_DOWN:
361             case ACTION_DOWN: {
362                 if (enableLog()) {
363                     Log.d(TAG, "ACTION_DOWN mLastRectTouched: " + mLastRectTouched);
364                 }
365                 if (mLastRectTouched != null) {
366                     return;
367                 }
368 
369                 for (OrientationRectF rect : mSwipeTouchRegions.values()) {
370                     if (enableLog()) {
371                         Log.d(TAG, "ACTION_DOWN rect: " + rect);
372                     }
373                     if (rect == null) {
374                         continue;
375                     }
376                     if (rect.applyTransformFromRotation(
377                             event, mCachedDisplayInfo.rotation, false)) {
378                         mLastRectTouched = rect;
379                         mActiveTouchRotation = rect.getRotation();
380                         if (mEnableMultipleRegions
381                                 && mCachedDisplayInfo.rotation == mActiveTouchRotation) {
382                             // TODO(b/154580671) might make this block unnecessary
383                             // Start a touch session for the default nav region for the display
384                             mQuickStepStartingRotation = mLastRectTouched.getRotation();
385                             resetSwipeRegions();
386                         }
387                         if (enableLog()) {
388                             Log.d(TAG, "set active region: " + rect);
389                         }
390                         return;
391                     }
392                 }
393                 break;
394             }
395         }
396     }
397 
enableLog()398     private boolean enableLog() {
399         return DEBUG || TestProtocol.sDebugTracing;
400     }
401 
dump(PrintWriter pw)402     public void dump(PrintWriter pw) {
403         pw.println("OrientationTouchTransformerState: ");
404         pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
405         pw.println("  lastTouchedRegion=" + mLastRectTouched);
406         pw.println("  multipleRegionsEnabled=" + mEnableMultipleRegions);
407         StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
408         for (CachedDisplayInfo key: mSwipeTouchRegions.keySet()) {
409             OrientationRectF rectF = mSwipeTouchRegions.get(key);
410             regions.append(rectF).append(" ");
411         }
412         pw.println(regions.toString());
413         pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
414         pw.println("  mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight);
415         pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
416     }
417 }
418