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