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