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