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