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