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