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