1 /* 2 * Copyright (C) 2020 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 package com.android.quickstep; 17 18 import static android.view.Display.DEFAULT_DISPLAY; 19 import static android.view.Surface.ROTATION_0; 20 21 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe; 22 import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll; 23 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN; 24 import static com.android.launcher3.util.DisplayController.CHANGE_ALL; 25 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE; 26 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION; 27 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS; 28 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 29 import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS; 30 31 import android.content.Context; 32 import android.content.res.Resources; 33 import android.view.MotionEvent; 34 import android.view.OrientationEventListener; 35 36 import com.android.launcher3.testing.shared.TestProtocol; 37 import com.android.launcher3.util.DisplayController; 38 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; 39 import com.android.launcher3.util.DisplayController.Info; 40 import com.android.launcher3.util.MainThreadInitializedObject; 41 import com.android.launcher3.util.NavigationMode; 42 import com.android.quickstep.util.RecentsOrientedState; 43 import com.android.systemui.shared.system.QuickStepContract; 44 import com.android.systemui.shared.system.TaskStackChangeListener; 45 import com.android.systemui.shared.system.TaskStackChangeListeners; 46 47 import java.io.PrintWriter; 48 import java.util.ArrayList; 49 50 /** 51 * Helper class for transforming touch events 52 */ 53 public class RotationTouchHelper implements DisplayInfoChangeListener { 54 55 public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE = 56 new MainThreadInitializedObject<>(RotationTouchHelper::new); 57 58 private OrientationTouchTransformer mOrientationTouchTransformer; 59 private DisplayController mDisplayController; 60 private int mDisplayId; 61 private int mDisplayRotation; 62 63 private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>(); 64 65 private NavigationMode mMode = THREE_BUTTONS; 66 67 private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() { 68 @Override 69 public void onRecentTaskListFrozenChanged(boolean frozen) { 70 mTaskListFrozen = frozen; 71 if (frozen || mInOverview) { 72 return; 73 } 74 enableMultipleRegions(false); 75 } 76 77 @Override 78 public void onActivityRotation(int displayId) { 79 // This always gets called before onDisplayInfoChanged() so we know how to process 80 // the rotation in that method. This is done to avoid having a race condition between 81 // the sensor readings and onDisplayInfoChanged() call 82 if (displayId != mDisplayId) { 83 return; 84 } 85 86 mPrioritizeDeviceRotation = true; 87 if (mInOverview) { 88 // reset, launcher must be rotating 89 mExitOverviewRunnable.run(); 90 } 91 } 92 }; 93 94 private Runnable mExitOverviewRunnable = new Runnable() { 95 @Override 96 public void run() { 97 mInOverview = false; 98 enableMultipleRegions(false); 99 } 100 }; 101 102 /** 103 * Used to listen for when the device rotates into the orientation of the current foreground 104 * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then 105 * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust 106 * the navbar. 107 */ 108 private OrientationEventListener mOrientationListener; 109 private int mSensorRotation = ROTATION_0; 110 /** 111 * This is the configuration of the foreground app or the app that will be in the foreground 112 * once a quickstep gesture finishes. 113 */ 114 private int mCurrentAppRotation = -1; 115 /** 116 * This flag is set to true when the device physically changes orientations. When true, we will 117 * always report the current rotation of the foreground app whenever the display changes, as it 118 * would indicate the user's intention to rotate the foreground app. 119 */ 120 private boolean mPrioritizeDeviceRotation = false; 121 private Runnable mOnDestroyFrozenTaskRunnable; 122 /** 123 * Set to true when user swipes to recents. In recents, we ignore the state of the recents 124 * task list being frozen or not to allow the user to keep interacting with nav bar rotation 125 * they went into recents with as opposed to defaulting to the default display rotation. 126 * TODO: (b/156984037) For when user rotates after entering overview 127 */ 128 private boolean mInOverview; 129 private boolean mTaskListFrozen; 130 private final Context mContext; 131 132 /** 133 * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests 134 * where multiple instances of RotationTouchHelper are being created. b/177316094 135 */ 136 private boolean mNeedsInit = true; 137 RotationTouchHelper(Context context)138 private RotationTouchHelper(Context context) { 139 mContext = context; 140 if (mNeedsInit) { 141 init(); 142 } 143 } 144 init()145 public void init() { 146 if (!mNeedsInit) { 147 return; 148 } 149 mDisplayController = DisplayController.INSTANCE.get(mContext); 150 Resources resources = mContext.getResources(); 151 mDisplayId = DEFAULT_DISPLAY; 152 153 mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode, 154 () -> QuickStepContract.getWindowCornerRadius(mContext)); 155 156 // Register for navigation mode changes 157 mDisplayController.addChangeListener(this); 158 DisplayController.Info info = mDisplayController.getInfo(); 159 onDisplayInfoChangedInternal(info, CHANGE_ALL, info.navigationMode.hasGestures); 160 runOnDestroy(() -> mDisplayController.removeChangeListener(this)); 161 162 mOrientationListener = new OrientationEventListener(mContext) { 163 @Override 164 public void onOrientationChanged(int degrees) { 165 int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees, 166 mSensorRotation); 167 if (newRotation == mSensorRotation) { 168 return; 169 } 170 171 mSensorRotation = newRotation; 172 mPrioritizeDeviceRotation = true; 173 174 if (newRotation == mCurrentAppRotation) { 175 // When user rotates device to the orientation of the foreground app after 176 // quickstepping 177 toggleSecondaryNavBarsForRotation(); 178 } 179 } 180 }; 181 mNeedsInit = false; 182 } 183 setupOrientationSwipeHandler()184 private void setupOrientationSwipeHandler() { 185 TaskStackChangeListeners.getInstance().registerTaskStackListener(mFrozenTaskListener); 186 mOnDestroyFrozenTaskRunnable = () -> TaskStackChangeListeners.getInstance() 187 .unregisterTaskStackListener(mFrozenTaskListener); 188 runOnDestroy(mOnDestroyFrozenTaskRunnable); 189 } 190 destroyOrientationSwipeHandlerCallback()191 private void destroyOrientationSwipeHandlerCallback() { 192 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mFrozenTaskListener); 193 mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable); 194 } 195 runOnDestroy(Runnable action)196 private void runOnDestroy(Runnable action) { 197 mOnDestroyActions.add(action); 198 } 199 200 /** 201 * Cleans up all the registered listeners and receivers. 202 */ destroy()203 public void destroy() { 204 for (Runnable r : mOnDestroyActions) { 205 r.run(); 206 } 207 mNeedsInit = true; 208 } 209 isTaskListFrozen()210 public boolean isTaskListFrozen() { 211 return mTaskListFrozen; 212 } 213 touchInAssistantRegion(MotionEvent ev)214 public boolean touchInAssistantRegion(MotionEvent ev) { 215 return mOrientationTouchTransformer.touchInAssistantRegion(ev); 216 } 217 touchInOneHandedModeRegion(MotionEvent ev)218 public boolean touchInOneHandedModeRegion(MotionEvent ev) { 219 return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev); 220 } 221 222 /** 223 * Updates the regions for detecting the swipe up/quickswitch and assistant gestures. 224 */ updateGestureTouchRegions()225 public void updateGestureTouchRegions() { 226 if (!mMode.hasGestures) { 227 return; 228 } 229 230 mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo()); 231 } 232 233 /** 234 * @return whether the coordinates of the {@param event} is in the swipe up gesture region. 235 */ isInSwipeUpTouchRegion(MotionEvent event)236 public boolean isInSwipeUpTouchRegion(MotionEvent event) { 237 return isInSwipeUpTouchRegion(event, 0); 238 } 239 240 /** 241 * @return whether the coordinates of the {@param event} with the given {@param pointerIndex} 242 * is in the swipe up gesture region. 243 */ isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex)244 public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) { 245 if (isTrackpadScroll(event)) { 246 return false; 247 } 248 if (isTrackpadMultiFingerSwipe(event)) { 249 return true; 250 } 251 return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex), 252 event.getY(pointerIndex)); 253 } 254 255 @Override onDisplayInfoChanged(Context context, Info info, int flags)256 public void onDisplayInfoChanged(Context context, Info info, int flags) { 257 onDisplayInfoChangedInternal(info, flags, false); 258 } 259 onDisplayInfoChangedInternal(Info info, int flags, boolean forceRegister)260 private void onDisplayInfoChangedInternal(Info info, int flags, boolean forceRegister) { 261 if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN | CHANGE_NAVIGATION_MODE 262 | CHANGE_SUPPORTED_BOUNDS)) != 0) { 263 mDisplayRotation = info.rotation; 264 265 if (mMode.hasGestures) { 266 updateGestureTouchRegions(); 267 mOrientationTouchTransformer.createOrAddTouchRegion(info); 268 mCurrentAppRotation = mDisplayRotation; 269 270 /* Update nav bars on the following: 271 * a) if this is coming from an activity rotation OR 272 * aa) we launch an app in the orientation that user is already in 273 * b) We're not in overview, since overview will always be portrait (w/o home 274 * rotation) 275 * c) We're actively in quickswitch mode 276 */ 277 if ((mPrioritizeDeviceRotation 278 || mCurrentAppRotation == mSensorRotation) 279 // switch to an app of orientation user is in 280 && !mInOverview 281 && mTaskListFrozen) { 282 toggleSecondaryNavBarsForRotation(); 283 } 284 } 285 } 286 287 if ((flags & CHANGE_NAVIGATION_MODE) != 0) { 288 NavigationMode newMode = info.navigationMode; 289 mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(), 290 mContext.getResources()); 291 292 if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) { 293 setupOrientationSwipeHandler(); 294 } else if (mMode.hasGestures && !newMode.hasGestures) { 295 destroyOrientationSwipeHandlerCallback(); 296 } 297 298 mMode = newMode; 299 } 300 } 301 getDisplayRotation()302 public int getDisplayRotation() { 303 return mDisplayRotation; 304 } 305 306 /** 307 * Sets the gestural height. 308 */ setGesturalHeight(int newGesturalHeight)309 void setGesturalHeight(int newGesturalHeight) { 310 mOrientationTouchTransformer.setGesturalHeight( 311 newGesturalHeight, mDisplayController.getInfo(), mContext.getResources()); 312 } 313 314 /** 315 * *May* apply a transform on the motion event if it lies in the nav bar region for another 316 * orientation that is currently being tracked as a part of quickstep 317 */ setOrientationTransformIfNeeded(MotionEvent event)318 void setOrientationTransformIfNeeded(MotionEvent event) { 319 // negative coordinates bug b/143901881 320 if (event.getX() < 0 || event.getY() < 0) { 321 event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY())); 322 } 323 mOrientationTouchTransformer.transform(event); 324 } 325 enableMultipleRegions(boolean enable)326 private void enableMultipleRegions(boolean enable) { 327 mOrientationTouchTransformer.enableMultipleRegions(enable, mDisplayController.getInfo()); 328 notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation()); 329 if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) { 330 // Clear any previous state from sensor manager 331 mSensorRotation = mCurrentAppRotation; 332 UI_HELPER_EXECUTOR.execute(mOrientationListener::enable); 333 } else { 334 UI_HELPER_EXECUTOR.execute(mOrientationListener::disable); 335 } 336 } 337 onStartGesture()338 public void onStartGesture() { 339 if (mTaskListFrozen) { 340 // Prioritize whatever nav bar user touches once in quickstep 341 // This case is specifically when user changes what nav bar they are using mid 342 // quickswitch session before tasks list is unfrozen 343 notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation()); 344 } 345 } 346 onEndTargetCalculated(GestureState.GestureEndTarget endTarget, BaseActivityInterface activityInterface)347 void onEndTargetCalculated(GestureState.GestureEndTarget endTarget, 348 BaseActivityInterface activityInterface) { 349 if (endTarget == GestureState.GestureEndTarget.RECENTS) { 350 mInOverview = true; 351 if (!mTaskListFrozen) { 352 // If we're in landscape w/o ever quickswitching, show the navbar in landscape 353 enableMultipleRegions(true); 354 } 355 activityInterface.onExitOverview(this, mExitOverviewRunnable); 356 } else if (endTarget == GestureState.GestureEndTarget.HOME 357 || endTarget == GestureState.GestureEndTarget.ALL_APPS) { 358 enableMultipleRegions(false); 359 } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) { 360 if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) { 361 // First gesture to start quickswitch 362 enableMultipleRegions(true); 363 } else { 364 notifySysuiOfCurrentRotation( 365 mOrientationTouchTransformer.getCurrentActiveRotation()); 366 } 367 368 // A new gesture is starting, reset the current device rotation 369 // This is done under the assumption that the user won't rotate the phone and then 370 // quickswitch in the old orientation. 371 mPrioritizeDeviceRotation = false; 372 } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) { 373 if (!mTaskListFrozen) { 374 // touched nav bar but didn't go anywhere and not quickswitching, do nothing 375 return; 376 } 377 notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation()); 378 } 379 } 380 notifySysuiOfCurrentRotation(int rotation)381 private void notifySysuiOfCurrentRotation(int rotation) { 382 UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext) 383 .notifyPrioritizedRotation(rotation)); 384 } 385 386 /** 387 * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then 388 * notifies system UI of the primary rotation the user is interacting with 389 */ toggleSecondaryNavBarsForRotation()390 private void toggleSecondaryNavBarsForRotation() { 391 mOrientationTouchTransformer.setSingleActiveRegion(mDisplayController.getInfo()); 392 notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation()); 393 } 394 getCurrentActiveRotation()395 public int getCurrentActiveRotation() { 396 if (!mMode.hasGestures) { 397 // touch rotation should always match that of display for 3 button 398 return mDisplayRotation; 399 } 400 return mOrientationTouchTransformer.getCurrentActiveRotation(); 401 } 402 dump(PrintWriter pw)403 public void dump(PrintWriter pw) { 404 pw.println("RotationTouchHelper:"); 405 pw.println(" currentActiveRotation=" + getCurrentActiveRotation()); 406 pw.println(" displayRotation=" + getDisplayRotation()); 407 mOrientationTouchTransformer.dump(pw); 408 } 409 getOrientationTouchTransformer()410 public OrientationTouchTransformer getOrientationTouchTransformer() { 411 return mOrientationTouchTransformer; 412 } 413 } 414