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