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