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