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