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 17 package com.android.quickstep.util; 18 19 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; 20 import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; 21 import static android.view.Surface.ROTATION_0; 22 import static android.view.Surface.ROTATION_180; 23 import static android.view.Surface.ROTATION_270; 24 import static android.view.Surface.ROTATION_90; 25 26 import static com.android.launcher3.logging.LoggerUtils.extractObjectNameAndAddress; 27 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; 28 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 29 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS; 30 31 import static java.lang.annotation.RetentionPolicy.SOURCE; 32 33 import android.content.ContentResolver; 34 import android.content.Context; 35 import android.content.SharedPreferences; 36 import android.content.res.Resources; 37 import android.database.ContentObserver; 38 import android.graphics.Matrix; 39 import android.graphics.PointF; 40 import android.graphics.Rect; 41 import android.os.Handler; 42 import android.provider.Settings; 43 import android.util.Log; 44 import android.view.MotionEvent; 45 import android.view.OrientationEventListener; 46 import android.view.Surface; 47 48 import androidx.annotation.IntDef; 49 import androidx.annotation.NonNull; 50 51 import com.android.launcher3.DeviceProfile; 52 import com.android.launcher3.InvariantDeviceProfile; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.testing.TestProtocol; 55 import com.android.launcher3.touch.PagedOrientationHandler; 56 import com.android.launcher3.util.WindowBounds; 57 import com.android.quickstep.BaseActivityInterface; 58 import com.android.quickstep.SysUINavigationMode; 59 60 import java.lang.annotation.Retention; 61 import java.util.function.IntConsumer; 62 63 /** 64 * Container to hold orientation/rotation related information for Launcher. 65 * This is not meant to be an abstraction layer for applying different functionality between 66 * the different orientation/rotations. For that see {@link PagedOrientationHandler} 67 * 68 * This class has initial default state assuming the device and foreground app have 69 * no ({@link Surface#ROTATION_0} rotation. 70 */ 71 public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener { 72 73 private static final String TAG = "RecentsOrientedState"; 74 private static final boolean DEBUG = true; 75 76 private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) { 77 @Override 78 public void onChange(boolean selfChange) { 79 updateAutoRotateSetting(); 80 } 81 }; 82 @Retention(SOURCE) 83 @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270}) 84 public @interface SurfaceRotation {} 85 86 private PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT; 87 88 private @SurfaceRotation int mTouchRotation = ROTATION_0; 89 private @SurfaceRotation int mDisplayRotation = ROTATION_0; 90 private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0; 91 private @SurfaceRotation int mRecentsRotation = ROTATION_0 - 1; 92 93 // Launcher activity supports multiple orientation, but fallback activity does not 94 private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0; 95 // Multiple orientation is only supported if density is < 600 96 private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY = 1 << 1; 97 // Shared prefs for rotation, only if activity supports it 98 private static final int FLAG_HOME_ROTATION_ALLOWED_IN_PREFS = 1 << 2; 99 // If the user has enabled system rotation 100 private static final int FLAG_SYSTEM_ROTATION_ALLOWED = 1 << 3; 101 // Multiple orientation is not supported in multiwindow mode 102 private static final int FLAG_MULTIWINDOW_ROTATION_ALLOWED = 1 << 4; 103 // Whether to rotation sensor is supported on the device 104 private static final int FLAG_ROTATION_WATCHER_SUPPORTED = 1 << 5; 105 // Whether to enable rotation watcher when multi-rotation is supported 106 private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6; 107 // Enable home rotation for UI tests, ignoring home rotation value from prefs 108 private static final int FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING = 1 << 7; 109 // Whether the swipe gesture is running, so the recents would stay locked in the 110 // current orientation 111 private static final int FLAG_SWIPE_UP_NOT_RUNNING = 1 << 8; 112 113 private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE = 114 FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY 115 | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY; 116 117 // State for which rotation watcher will be enabled. We skip it when home rotation or 118 // multi-window is enabled as in that case, activity itself rotates. 119 private static final int VALUE_ROTATION_WATCHER_ENABLED = 120 MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED 121 | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED 122 | FLAG_SWIPE_UP_NOT_RUNNING; 123 124 private final Context mContext; 125 private final ContentResolver mContentResolver; 126 private final SharedPreferences mSharedPrefs; 127 private final OrientationEventListener mOrientationListener; 128 129 private final Matrix mTmpMatrix = new Matrix(); 130 131 private int mFlags; 132 private int mPreviousRotation = ROTATION_0; 133 134 /** 135 * @param rotationChangeListener Callback for receiving rotation events when rotation watcher 136 * is enabled 137 * @see #setRotationWatcherEnabled(boolean) 138 */ RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy, IntConsumer rotationChangeListener)139 public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy, 140 IntConsumer rotationChangeListener) { 141 mContext = context; 142 mContentResolver = context.getContentResolver(); 143 mSharedPrefs = Utilities.getPrefs(context); 144 mOrientationListener = new OrientationEventListener(context) { 145 @Override 146 public void onOrientationChanged(int degrees) { 147 int newRotation = getRotationForUserDegreesRotated(degrees, mPreviousRotation); 148 if (newRotation != mPreviousRotation) { 149 mPreviousRotation = newRotation; 150 rotationChangeListener.accept(newRotation); 151 } 152 } 153 }; 154 155 mFlags = sizeStrategy.rotationSupportedByActivity 156 ? FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY : 0; 157 158 Resources res = context.getResources(); 159 int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp 160 * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE; 161 if (originalSmallestWidth < 600) { 162 mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY; 163 } 164 mFlags |= FLAG_SWIPE_UP_NOT_RUNNING; 165 initFlags(); 166 } 167 168 /** 169 * Sets the rotation for the recents activity, which could affect the appearance of task view. 170 * @see #update(int, int) 171 */ setRecentsRotation(@urfaceRotation int recentsRotation)172 public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) { 173 mRecentsRotation = recentsRotation; 174 return update(mTouchRotation, mDisplayRotation); 175 } 176 177 /** 178 * Sets if the host is in multi-window mode 179 */ setMultiWindowMode(boolean isMultiWindow)180 public void setMultiWindowMode(boolean isMultiWindow) { 181 setFlag(FLAG_MULTIWINDOW_ROTATION_ALLOWED, isMultiWindow); 182 } 183 184 /** 185 * Sets if the swipe up gesture is currently running or not 186 */ setGestureActive(boolean isGestureActive)187 public boolean setGestureActive(boolean isGestureActive) { 188 setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive); 189 return update(mTouchRotation, mDisplayRotation); 190 } 191 192 /** 193 * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler} 194 * @param touchRotation The rotation the nav bar region that is touched is in 195 * @param displayRotation Rotation of the display/device 196 * 197 * @return true if there was any change in the internal state as a result of this call, 198 * false otherwise 199 */ update( @urfaceRotation int touchRotation, @SurfaceRotation int displayRotation)200 public boolean update( 201 @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) { 202 mRecentsActivityRotation = inferRecentsActivityRotation(displayRotation); 203 mDisplayRotation = displayRotation; 204 mTouchRotation = touchRotation; 205 mPreviousRotation = touchRotation; 206 207 PagedOrientationHandler oldHandler = mOrientationHandler; 208 if (mRecentsActivityRotation == mTouchRotation 209 || (canRecentsActivityRotate() && (mFlags & FLAG_SWIPE_UP_NOT_RUNNING) != 0)) { 210 mOrientationHandler = PagedOrientationHandler.PORTRAIT; 211 if (DEBUG) { 212 Log.d(TAG, "current RecentsOrientedState: " + this); 213 } 214 } else if (mTouchRotation == ROTATION_90) { 215 mOrientationHandler = PagedOrientationHandler.LANDSCAPE; 216 } else if (mTouchRotation == ROTATION_270) { 217 mOrientationHandler = PagedOrientationHandler.SEASCAPE; 218 } else { 219 mOrientationHandler = PagedOrientationHandler.PORTRAIT; 220 } 221 if (DEBUG) { 222 Log.d(TAG, "current RecentsOrientedState: " + this); 223 } 224 return oldHandler != mOrientationHandler; 225 } 226 227 @SurfaceRotation inferRecentsActivityRotation(@urfaceRotation int displayRotation)228 private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) { 229 if (isRecentsActivityRotationAllowed()) { 230 return mRecentsRotation < ROTATION_0 ? displayRotation : mRecentsRotation; 231 } else { 232 return ROTATION_0; 233 } 234 } 235 setFlag(int mask, boolean enabled)236 private void setFlag(int mask, boolean enabled) { 237 boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation 238 && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED 239 && !canRecentsActivityRotate(); 240 if (enabled) { 241 mFlags |= mask; 242 } else { 243 mFlags &= ~mask; 244 } 245 246 boolean isRotationEnabled = !TestProtocol.sDisableSensorRotation 247 && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED 248 && !canRecentsActivityRotate(); 249 if (wasRotationEnabled != isRotationEnabled) { 250 UI_HELPER_EXECUTOR.execute(() -> { 251 if (isRotationEnabled) { 252 mOrientationListener.enable(); 253 } else { 254 mOrientationListener.disable(); 255 } 256 }); 257 } 258 } 259 260 @Override onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s)261 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { 262 if (ALLOW_ROTATION_PREFERENCE_KEY.equals(s)) { 263 updateHomeRotationSetting(); 264 } 265 } 266 updateAutoRotateSetting()267 private void updateAutoRotateSetting() { 268 setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver, 269 Settings.System.ACCELEROMETER_ROTATION, 1) == 1); 270 } 271 updateHomeRotationSetting()272 private void updateHomeRotationSetting() { 273 setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS, 274 mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false)); 275 } 276 initFlags()277 private void initFlags() { 278 SysUINavigationMode.Mode currentMode = SysUINavigationMode.getMode(mContext); 279 boolean rotationWatcherSupported = mOrientationListener.canDetectOrientation() && 280 currentMode != TWO_BUTTONS; 281 setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, rotationWatcherSupported); 282 283 // initialize external flags 284 updateAutoRotateSetting(); 285 updateHomeRotationSetting(); 286 } 287 288 /** 289 * Initializes any system values and registers corresponding change listeners. It must be 290 * paired with {@link #destroyListeners()} call 291 */ initListeners()292 public void initListeners() { 293 if (isMultipleOrientationSupportedByDevice()) { 294 mSharedPrefs.registerOnSharedPreferenceChangeListener(this); 295 mContentResolver.registerContentObserver( 296 Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), 297 false, mSystemAutoRotateObserver); 298 } 299 initFlags(); 300 } 301 302 /** 303 * Unregisters any previously registered listeners. 304 */ destroyListeners()305 public void destroyListeners() { 306 if (isMultipleOrientationSupportedByDevice()) { 307 mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this); 308 mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver); 309 } 310 setRotationWatcherEnabled(false); 311 } 312 forceAllowRotationForTesting(boolean forceAllow)313 public void forceAllowRotationForTesting(boolean forceAllow) { 314 setFlag(FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING, forceAllow); 315 } 316 317 @SurfaceRotation getDisplayRotation()318 public int getDisplayRotation() { 319 return mDisplayRotation; 320 } 321 322 @SurfaceRotation getTouchRotation()323 public int getTouchRotation() { 324 return mTouchRotation; 325 } 326 327 @SurfaceRotation getRecentsActivityRotation()328 public int getRecentsActivityRotation() { 329 return mRecentsActivityRotation; 330 } 331 isMultipleOrientationSupportedByDevice()332 public boolean isMultipleOrientationSupportedByDevice() { 333 return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE) 334 == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE; 335 } 336 isRecentsActivityRotationAllowed()337 public boolean isRecentsActivityRotationAllowed() { 338 // Activity rotation is allowed if the multi-simulated-rotation is not supported 339 // (fallback recents or tablets) or activity rotation is enabled by various settings. 340 return ((mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE) 341 != MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE) 342 || (mFlags & (FLAG_HOME_ROTATION_ALLOWED_IN_PREFS 343 | FLAG_MULTIWINDOW_ROTATION_ALLOWED 344 | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0; 345 } 346 347 /** 348 * Returns true if the activity can rotate, if allowed by system rotation settings 349 */ canRecentsActivityRotate()350 public boolean canRecentsActivityRotate() { 351 return (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0 && isRecentsActivityRotationAllowed(); 352 } 353 354 /** 355 * Enables or disables the rotation watcher for listening to rotation callbacks 356 */ setRotationWatcherEnabled(boolean isEnabled)357 public void setRotationWatcherEnabled(boolean isEnabled) { 358 setFlag(FLAG_ROTATION_WATCHER_ENABLED, isEnabled); 359 } 360 361 /** 362 * Returns the scale and pivot so that the provided taskRect can fit the provided full size 363 */ getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot)364 public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) { 365 Rect insets = dp.getInsets(); 366 float fullWidth = dp.widthPx - insets.left - insets.right; 367 float fullHeight = dp.heightPx - insets.top - insets.bottom; 368 369 if (dp.isMultiWindowMode) { 370 WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext); 371 outPivot.set(bounds.availableSize.x, bounds.availableSize.y); 372 } else { 373 outPivot.set(fullWidth, fullHeight); 374 } 375 float scale = Math.min(outPivot.x / taskView.width(), outPivot.y / taskView.height()); 376 // We also scale the preview as part of fullScreenParams, so account for that as well. 377 if (fullWidth > 0) { 378 scale = scale * dp.widthPx / fullWidth; 379 } 380 381 if (scale == 1) { 382 outPivot.set(fullWidth / 2, fullHeight / 2); 383 } else if (dp.isMultiWindowMode) { 384 float denominator = 1 / (scale - 1); 385 // Ensure that the task aligns to right bottom for the root view 386 float y = (scale * taskView.bottom - fullHeight) * denominator; 387 float x = (scale * taskView.right - fullWidth) * denominator; 388 outPivot.set(x, y); 389 } else { 390 float factor = scale / (scale - 1); 391 outPivot.set(taskView.left * factor, taskView.top * factor); 392 } 393 return scale; 394 } 395 getOrientationHandler()396 public PagedOrientationHandler getOrientationHandler() { 397 return mOrientationHandler; 398 } 399 400 /** 401 * For landscape, since the navbar is already in a vertical position, we don't have to do any 402 * rotations as the change in Y coordinate is what is read. We only flip the sign of the 403 * y coordinate to make it match existing behavior of swipe to the top to go previous 404 */ flipVertical(MotionEvent ev)405 public void flipVertical(MotionEvent ev) { 406 mTmpMatrix.setScale(1, -1); 407 ev.transform(mTmpMatrix); 408 } 409 410 /** 411 * Creates a matrix to transform the given motion event specified by degrees. 412 * If inverse is {@code true}, the inverse of that matrix will be applied 413 */ transformEvent(float degrees, MotionEvent ev, boolean inverse)414 public void transformEvent(float degrees, MotionEvent ev, boolean inverse) { 415 mTmpMatrix.setRotate(inverse ? -degrees : degrees); 416 ev.transform(mTmpMatrix); 417 418 // TODO: Add scaling back in based on degrees 419 /* 420 if (getWidth() > 0 && getHeight() > 0) { 421 float scale = ((float) getWidth()) / getHeight(); 422 transform.postScale(scale, 1 / scale); 423 } 424 */ 425 } 426 427 @SurfaceRotation getRotationForUserDegreesRotated(float degrees, int currentRotation)428 public static int getRotationForUserDegreesRotated(float degrees, int currentRotation) { 429 if (degrees == ORIENTATION_UNKNOWN) { 430 return currentRotation; 431 } 432 433 int threshold = 70; 434 switch (currentRotation) { 435 case ROTATION_0: 436 if (degrees > 180 && degrees < (360 - threshold)) { 437 return ROTATION_90; 438 } 439 if (degrees < 180 && degrees > threshold) { 440 return ROTATION_270; 441 } 442 break; 443 case ROTATION_270: 444 if (degrees < (90 - threshold) || 445 (degrees > (270 + threshold) && degrees < 360)) { 446 return ROTATION_0; 447 } 448 if (degrees > (90 + threshold) && degrees < 180) { 449 return ROTATION_180; 450 } 451 // flip from seascape to landscape 452 if (degrees > (180 + threshold) && degrees < 360) { 453 return ROTATION_90; 454 } 455 break; 456 case ROTATION_180: 457 if (degrees < (180 - threshold)) { 458 return ROTATION_270; 459 } 460 if (degrees > (180 + threshold)) { 461 return ROTATION_90; 462 } 463 break; 464 case ROTATION_90: 465 if (degrees < (270 - threshold) && degrees > 90) { 466 return ROTATION_180; 467 } 468 if (degrees > (270 + threshold) && degrees < 360 469 || (degrees >= 0 && degrees < threshold)) { 470 return ROTATION_0; 471 } 472 // flip from landscape to seascape 473 if (degrees > threshold && degrees < 180) { 474 return ROTATION_270; 475 } 476 break; 477 } 478 479 return currentRotation; 480 } 481 isDisplayPhoneNatural()482 public boolean isDisplayPhoneNatural() { 483 return mDisplayRotation == Surface.ROTATION_0 || mDisplayRotation == Surface.ROTATION_180; 484 } 485 486 /** 487 * Posts the transformation on the matrix representing the provided display rotation 488 */ postDisplayRotation(@urfaceRotation int displayRotation, float screenWidth, float screenHeight, Matrix out)489 public static void postDisplayRotation(@SurfaceRotation int displayRotation, 490 float screenWidth, float screenHeight, Matrix out) { 491 switch (displayRotation) { 492 case ROTATION_0: 493 return; 494 case ROTATION_90: 495 out.postRotate(270); 496 out.postTranslate(0, screenWidth); 497 break; 498 case ROTATION_180: 499 out.postRotate(180); 500 out.postTranslate(screenHeight, screenWidth); 501 break; 502 case ROTATION_270: 503 out.postRotate(90); 504 out.postTranslate(screenHeight, 0); 505 break; 506 } 507 } 508 509 @NonNull 510 @Override toString()511 public String toString() { 512 boolean systemRotationOn = (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0; 513 return "[" 514 + "this=" + extractObjectNameAndAddress(super.toString()) 515 + " mOrientationHandler=" + 516 extractObjectNameAndAddress(mOrientationHandler.toString()) 517 + " mDisplayRotation=" + mDisplayRotation 518 + " mTouchRotation=" + mTouchRotation 519 + " mRecentsActivityRotation=" + mRecentsActivityRotation 520 + " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed() 521 + " mSystemRotation=" + systemRotationOn 522 + " mFlags=" + mFlags 523 + "]"; 524 } 525 526 /** 527 * Returns the device profile based on expected launcher rotation 528 */ getLauncherDeviceProfile()529 public DeviceProfile getLauncherDeviceProfile() { 530 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext); 531 // TODO also check the natural orientation is landscape or portrait 532 return (mRecentsActivityRotation == ROTATION_90 533 || mRecentsActivityRotation == ROTATION_270) 534 ? idp.landscapeProfile 535 : idp.portraitProfile; 536 } 537 } 538