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