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