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