1 /* 2 * Copyright 2021 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.systemui.shared.rotation; 18 19 import static android.content.pm.PackageManager.FEATURE_PC; 20 import static android.view.Display.DEFAULT_DISPLAY; 21 22 import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION; 23 import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ObjectAnimator; 28 import android.annotation.ColorInt; 29 import android.annotation.DrawableRes; 30 import android.annotation.SuppressLint; 31 import android.app.StatusBarManager; 32 import android.content.BroadcastReceiver; 33 import android.content.ContentResolver; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.IntentFilter; 37 import android.graphics.drawable.AnimatedVectorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.RemoteException; 42 import android.os.SystemProperties; 43 import android.provider.Settings; 44 import android.util.Log; 45 import android.view.HapticFeedbackConstants; 46 import android.view.IRotationWatcher; 47 import android.view.MotionEvent; 48 import android.view.Surface; 49 import android.view.View; 50 import android.view.WindowInsetsController; 51 import android.view.WindowManagerGlobal; 52 import android.view.accessibility.AccessibilityManager; 53 import android.view.animation.Interpolator; 54 import android.view.animation.LinearInterpolator; 55 56 import androidx.annotation.Nullable; 57 import androidx.annotation.WorkerThread; 58 59 import com.android.internal.annotations.VisibleForTesting; 60 import com.android.internal.logging.UiEvent; 61 import com.android.internal.logging.UiEventLogger; 62 import com.android.internal.logging.UiEventLoggerImpl; 63 import com.android.internal.view.RotationPolicy; 64 import com.android.systemui.shared.recents.utilities.Utilities; 65 import com.android.systemui.shared.recents.utilities.ViewRippler; 66 import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdatesCallback; 67 import com.android.systemui.shared.system.ActivityManagerWrapper; 68 import com.android.systemui.shared.system.TaskStackChangeListener; 69 import com.android.systemui.shared.system.TaskStackChangeListeners; 70 71 import java.io.PrintWriter; 72 import java.util.Optional; 73 import java.util.concurrent.Executor; 74 import java.util.concurrent.ThreadPoolExecutor; 75 import java.util.function.Supplier; 76 77 /** 78 * Contains logic that deals with showing a rotate suggestion button with animation. 79 */ 80 public class RotationButtonController { 81 public static final boolean DEBUG_ROTATION = false; 82 83 private static final String TAG = "RotationButtonController"; 84 private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100; 85 private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000; 86 private static final boolean OEM_DISALLOW_ROTATION_IN_SUW = 87 SystemProperties.getBoolean("ro.setupwizard.rotation_locked", false); 88 private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 89 90 private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3; 91 92 private final Context mContext; 93 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 94 private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); 95 private final ViewRippler mViewRippler = new ViewRippler(); 96 private final Supplier<Integer> mWindowRotationProvider; 97 private RotationButton mRotationButton; 98 99 private boolean mIsRecentsAnimationRunning; 100 private boolean mDocked; 101 private boolean mHomeRotationEnabled; 102 private int mLastRotationSuggestion; 103 private boolean mPendingRotationSuggestion; 104 private boolean mHoveringRotationSuggestion; 105 private final AccessibilityManager mAccessibilityManager; 106 private final TaskStackListenerImpl mTaskStackListener; 107 108 private boolean mListenersRegistered = false; 109 private boolean mRotationWatcherRegistered = false; 110 private boolean mIsNavigationBarShowing; 111 @SuppressLint("InlinedApi") 112 private @WindowInsetsController.Behavior 113 int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT; 114 private int mNavBarMode; 115 private boolean mTaskBarVisible = false; 116 private boolean mSkipOverrideUserLockPrefsOnce; 117 private final int mLightIconColor; 118 private final int mDarkIconColor; 119 120 @DrawableRes 121 private final int mIconCcwStart0ResId; 122 @DrawableRes 123 private final int mIconCcwStart90ResId; 124 @DrawableRes 125 private final int mIconCwStart0ResId; 126 @DrawableRes 127 private final int mIconCwStart90ResId; 128 /** Defaults to mainExecutor if not set via {@link #setBgExecutor(Executor)}. */ 129 private Executor mBgExecutor; 130 131 @DrawableRes 132 private int mIconResId; 133 134 private final Runnable mRemoveRotationProposal = 135 () -> setRotateSuggestionButtonState(false /* visible */); 136 private final Runnable mCancelPendingRotationProposal = 137 () -> mPendingRotationSuggestion = false; 138 private Animator mRotateHideAnimator; 139 140 private final BroadcastReceiver mDockedReceiver = new BroadcastReceiver() { 141 @Override 142 public void onReceive(Context context, Intent intent) { 143 updateDockedState(intent); 144 } 145 }; 146 147 private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() { 148 @WorkerThread 149 @Override 150 public void onRotationChanged(final int rotation) { 151 @Nullable Boolean rotationLocked = RotationPolicyUtil.isRotationLocked(mContext); 152 // We need this to be scheduled as early as possible to beat the redrawing of 153 // window in response to the orientation change. 154 mMainThreadHandler.postAtFrontOfQueue(() -> { 155 onRotationWatcherChanged(rotation, rotationLocked); 156 }); 157 } 158 }; 159 160 /** 161 * Determines if rotation suggestions disabled2 flag exists in flag 162 * 163 * @param disable2Flags see if rotation suggestion flag exists in this flag 164 * @return whether flag exists 165 */ hasDisable2RotateSuggestionFlag(int disable2Flags)166 public static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) { 167 return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0; 168 } 169 RotationButtonController(Context context, @ColorInt int lightIconColor, @ColorInt int darkIconColor, @DrawableRes int iconCcwStart0ResId, @DrawableRes int iconCcwStart90ResId, @DrawableRes int iconCwStart0ResId, @DrawableRes int iconCwStart90ResId, Supplier<Integer> windowRotationProvider)170 public RotationButtonController(Context context, 171 @ColorInt int lightIconColor, @ColorInt int darkIconColor, 172 @DrawableRes int iconCcwStart0ResId, 173 @DrawableRes int iconCcwStart90ResId, 174 @DrawableRes int iconCwStart0ResId, 175 @DrawableRes int iconCwStart90ResId, 176 Supplier<Integer> windowRotationProvider) { 177 178 mContext = context; 179 mLightIconColor = lightIconColor; 180 mDarkIconColor = darkIconColor; 181 182 mIconCcwStart0ResId = iconCcwStart0ResId; 183 mIconCcwStart90ResId = iconCcwStart90ResId; 184 mIconCwStart0ResId = iconCwStart0ResId; 185 mIconCwStart90ResId = iconCwStart90ResId; 186 mIconResId = mIconCcwStart90ResId; 187 188 mAccessibilityManager = AccessibilityManager.getInstance(context); 189 mTaskStackListener = new TaskStackListenerImpl(); 190 mWindowRotationProvider = windowRotationProvider; 191 192 mBgExecutor = context.getMainExecutor(); 193 } 194 setRotationButton(RotationButton rotationButton, RotationButtonUpdatesCallback updatesCallback)195 public void setRotationButton(RotationButton rotationButton, 196 RotationButtonUpdatesCallback updatesCallback) { 197 mRotationButton = rotationButton; 198 mRotationButton.setRotationButtonController(this); 199 mRotationButton.setOnClickListener(this::onRotateSuggestionClick); 200 mRotationButton.setOnHoverListener(this::onRotateSuggestionHover); 201 mRotationButton.setUpdatesCallback(updatesCallback); 202 } 203 getContext()204 public Context getContext() { 205 return mContext; 206 } 207 208 /** 209 * We should pass single threaded executor (rather than {@link ThreadPoolExecutor}) as we will 210 * make binder calls on that executor and ordering is vital. 211 */ setBgExecutor(Executor bgExecutor)212 public void setBgExecutor(Executor bgExecutor) { 213 mBgExecutor = bgExecutor; 214 } 215 216 /** 217 * Called during Taskbar initialization. 218 */ init()219 public void init() { 220 registerListeners(true /* registerRotationWatcher */); 221 if (mContext.getDisplay().getDisplayId() != DEFAULT_DISPLAY) { 222 // Currently there is no accelerometer sensor on non-default display, disable fixed 223 // rotation for non-default display 224 onDisable2FlagChanged(StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS); 225 } 226 } 227 228 /** 229 * Called during Taskbar uninitialization. 230 */ onDestroy()231 public void onDestroy() { 232 unregisterListeners(); 233 } 234 registerListeners(boolean registerRotationWatcher)235 public void registerListeners(boolean registerRotationWatcher) { 236 if (mListenersRegistered || getContext().getPackageManager().hasSystemFeature(FEATURE_PC)) { 237 return; 238 } 239 240 mListenersRegistered = true; 241 242 mBgExecutor.execute(() -> { 243 if (registerRotationWatcher) { 244 try { 245 WindowManagerGlobal.getWindowManagerService() 246 .watchRotation(mRotationWatcher, DEFAULT_DISPLAY); 247 mRotationWatcherRegistered = true; 248 } catch (IllegalArgumentException e) { 249 Log.w(TAG, "RegisterListeners for the display failed", e); 250 } catch (RemoteException e) { 251 Log.e(TAG, "RegisterListeners caught a RemoteException", e); 252 } 253 } 254 final Intent intent = mContext.registerReceiver(mDockedReceiver, 255 new IntentFilter(Intent.ACTION_DOCK_EVENT)); 256 mContext.getMainExecutor().execute(() -> updateDockedState(intent)); 257 }); 258 259 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 260 } 261 unregisterListeners()262 public void unregisterListeners() { 263 if (!mListenersRegistered) { 264 return; 265 } 266 267 mListenersRegistered = false; 268 269 mBgExecutor.execute(() -> { 270 try { 271 mContext.unregisterReceiver(mDockedReceiver); 272 } catch (IllegalArgumentException e) { 273 Log.e(TAG, "Docked receiver already unregistered", e); 274 } 275 276 if (mRotationWatcherRegistered) { 277 try { 278 WindowManagerGlobal.getWindowManagerService().removeRotationWatcher( 279 mRotationWatcher); 280 } catch (RemoteException e) { 281 Log.e(TAG, "UnregisterListeners caught a RemoteException", e); 282 } 283 } 284 }); 285 286 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 287 } 288 setRotationLockedAtAngle( @ullable Boolean isLocked, int rotationSuggestion, String caller)289 public void setRotationLockedAtAngle( 290 @Nullable Boolean isLocked, int rotationSuggestion, String caller) { 291 if (isLocked == null) { 292 // Ignore if we can't read the setting for the current user 293 return; 294 } 295 RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isLocked, 296 /* rotation= */ rotationSuggestion, caller); 297 } 298 setRotateSuggestionButtonState(boolean visible)299 public void setRotateSuggestionButtonState(boolean visible) { 300 setRotateSuggestionButtonState(visible, false /* force */); 301 } 302 setRotateSuggestionButtonState(final boolean visible, final boolean force)303 void setRotateSuggestionButtonState(final boolean visible, final boolean force) { 304 // At any point the button can become invisible because an a11y service became active. 305 // Similarly, a call to make the button visible may be rejected because an a11y service is 306 // active. Must account for this. 307 // Rerun a show animation to indicate change but don't rerun a hide animation 308 if (!visible && !mRotationButton.isVisible()) return; 309 310 final View view = mRotationButton.getCurrentView(); 311 if (view == null) return; 312 313 final Drawable currentDrawable = mRotationButton.getImageDrawable(); 314 if (currentDrawable == null) return; 315 316 // Clear any pending suggestion flag as it has either been nullified or is being shown 317 mPendingRotationSuggestion = false; 318 mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal); 319 320 // Handle the visibility change and animation 321 if (visible) { // Appear and change (cannot force) 322 // Stop and clear any currently running hide animations 323 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { 324 mRotateHideAnimator.cancel(); 325 } 326 mRotateHideAnimator = null; 327 328 // Reset the alpha if any has changed due to hide animation 329 view.setAlpha(1f); 330 331 // Run the rotate icon's animation if it has one 332 if (currentDrawable instanceof AnimatedVectorDrawable) { 333 ((AnimatedVectorDrawable) currentDrawable).reset(); 334 ((AnimatedVectorDrawable) currentDrawable).start(); 335 } 336 337 // TODO(b/187754252): No idea why this doesn't work. If we remove the "false" 338 // we see the animation show the pressed state... but it only shows the first time. 339 if (!isRotateSuggestionIntroduced()) mViewRippler.start(view); 340 341 // Set visibility unless a11y service is active. 342 mRotationButton.show(); 343 } else { // Hide 344 mViewRippler.stop(); // Prevent any pending ripples, force hide or not 345 346 if (force) { 347 // If a hide animator is running stop it and make invisible 348 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { 349 mRotateHideAnimator.pause(); 350 } 351 mRotationButton.hide(); 352 return; 353 } 354 355 // Don't start any new hide animations if one is running 356 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; 357 358 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f); 359 fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS); 360 fadeOut.setInterpolator(LINEAR_INTERPOLATOR); 361 fadeOut.addListener(new AnimatorListenerAdapter() { 362 @Override 363 public void onAnimationEnd(Animator animation) { 364 mRotationButton.hide(); 365 } 366 }); 367 368 mRotateHideAnimator = fadeOut; 369 fadeOut.start(); 370 } 371 } 372 setDarkIntensity(float darkIntensity)373 public void setDarkIntensity(float darkIntensity) { 374 mRotationButton.setDarkIntensity(darkIntensity); 375 } 376 setRecentsAnimationRunning(boolean running)377 public void setRecentsAnimationRunning(boolean running) { 378 mIsRecentsAnimationRunning = running; 379 updateRotationButtonStateInOverview(); 380 } 381 setHomeRotationEnabled(boolean enabled)382 public void setHomeRotationEnabled(boolean enabled) { 383 mHomeRotationEnabled = enabled; 384 updateRotationButtonStateInOverview(); 385 } 386 updateDockedState(Intent intent)387 private void updateDockedState(Intent intent) { 388 if (intent == null) { 389 return; 390 } 391 392 mDocked = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED) 393 != Intent.EXTRA_DOCK_STATE_UNDOCKED; 394 } 395 updateRotationButtonStateInOverview()396 private void updateRotationButtonStateInOverview() { 397 if (mIsRecentsAnimationRunning && !mHomeRotationEnabled) { 398 setRotateSuggestionButtonState(false, true /* hideImmediately */); 399 } 400 } 401 onRotationProposal(int rotation, boolean isValid)402 public void onRotationProposal(int rotation, boolean isValid) { 403 boolean isUserSetupComplete = Settings.Secure.getInt(mContext.getContentResolver(), 404 Settings.Secure.USER_SETUP_COMPLETE, 0) != 0; 405 if (!isUserSetupComplete && OEM_DISALLOW_ROTATION_IN_SUW) { 406 return; 407 } 408 409 int windowRotation = mWindowRotationProvider.get(); 410 411 if (!mRotationButton.acceptRotationProposal()) { 412 return; 413 } 414 415 if (!mHomeRotationEnabled && mIsRecentsAnimationRunning) { 416 return; 417 } 418 419 // This method will be called on rotation suggestion changes even if the proposed rotation 420 // is not valid for the top app. Use invalid rotation choices as a signal to remove the 421 // rotate button if shown. 422 if (!isValid) { 423 setRotateSuggestionButtonState(false /* visible */); 424 return; 425 } 426 427 // If window rotation matches suggested rotation, remove any current suggestions 428 if (rotation == windowRotation) { 429 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 430 setRotateSuggestionButtonState(false /* visible */); 431 return; 432 } 433 434 // Prepare to show the navbar icon by updating the icon style to change anim params 435 Log.i(TAG, "onRotationProposal(rotation=" + rotation + ")"); 436 mLastRotationSuggestion = rotation; // Remember rotation for click 437 final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation); 438 if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) { 439 mIconResId = rotationCCW ? mIconCcwStart0ResId : mIconCwStart0ResId; 440 } else { // 90 or 270 441 mIconResId = rotationCCW ? mIconCcwStart90ResId : mIconCwStart90ResId; 442 } 443 mRotationButton.updateIcon(mLightIconColor, mDarkIconColor); 444 445 if (canShowRotationButton()) { 446 // The navbar is visible / it's in visual immersive mode, so show the icon right away 447 showAndLogRotationSuggestion(); 448 } else { 449 // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become 450 // visible given some time limit. 451 mPendingRotationSuggestion = true; 452 mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal); 453 mMainThreadHandler.postDelayed(mCancelPendingRotationProposal, 454 NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS); 455 } 456 } 457 458 /** 459 * Called when the rotation watcher rotation changes, either from the watcher registered 460 * internally in this class, or a signal propagated from NavBarHelper. 461 */ onRotationWatcherChanged(int rotation, @Nullable Boolean isRotationLocked)462 public void onRotationWatcherChanged(int rotation, @Nullable Boolean isRotationLocked) { 463 if (!mListenersRegistered) { 464 // Ignore if not registered 465 return; 466 } 467 468 // If the screen rotation changes while locked, potentially update lock to flow with 469 // new screen rotation and hide any showing suggestions. 470 if (isRotationLocked == null) { 471 // Ignore if we can't read the setting for the current user 472 return; 473 } 474 // The isVisible check makes the rotation button disappear when we are not locked 475 // (e.g. for tabletop auto-rotate). 476 if (isRotationLocked || mRotationButton.isVisible()) { 477 // Do not allow a change in rotation to set user rotation when docked. 478 if (shouldOverrideUserLockPrefs(rotation) && isRotationLocked && !mDocked) { 479 setRotationLockedAtAngle(true, rotation, /* caller= */ 480 "RotationButtonController#onRotationWatcherChanged"); 481 } 482 setRotateSuggestionButtonState(false /* visible */, true /* forced */); 483 } 484 } 485 onDisable2FlagChanged(int state2)486 public void onDisable2FlagChanged(int state2) { 487 final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2); 488 if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled(); 489 } 490 onNavigationModeChanged(int mode)491 public void onNavigationModeChanged(int mode) { 492 mNavBarMode = mode; 493 } 494 onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior)495 public void onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior) { 496 if (DEFAULT_DISPLAY != displayId) { 497 return; 498 } 499 500 if (mBehavior != behavior) { 501 mBehavior = behavior; 502 showPendingRotationButtonIfNeeded(); 503 } 504 } 505 onNavigationBarWindowVisibilityChange(boolean showing)506 public void onNavigationBarWindowVisibilityChange(boolean showing) { 507 if (mIsNavigationBarShowing != showing) { 508 mIsNavigationBarShowing = showing; 509 showPendingRotationButtonIfNeeded(); 510 } 511 } 512 onTaskbarStateChange(boolean visible, boolean stashed)513 public void onTaskbarStateChange(boolean visible, boolean stashed) { 514 mTaskBarVisible = visible; 515 if (getRotationButton() == null) { 516 return; 517 } 518 getRotationButton().onTaskbarStateChanged(visible, stashed); 519 } 520 showPendingRotationButtonIfNeeded()521 private void showPendingRotationButtonIfNeeded() { 522 if (canShowRotationButton() && mPendingRotationSuggestion) { 523 showAndLogRotationSuggestion(); 524 } 525 } 526 527 /** 528 * Return true when either the task bar is visible or it's in visual immersive mode. 529 */ 530 @SuppressLint("InlinedApi") 531 @VisibleForTesting canShowRotationButton()532 boolean canShowRotationButton() { 533 return mIsNavigationBarShowing 534 || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT 535 || isGesturalMode(mNavBarMode); 536 } 537 538 @DrawableRes getIconResId()539 public int getIconResId() { 540 return mIconResId; 541 } 542 543 @ColorInt getLightIconColor()544 public int getLightIconColor() { 545 return mLightIconColor; 546 } 547 548 @ColorInt getDarkIconColor()549 public int getDarkIconColor() { 550 return mDarkIconColor; 551 } 552 dumpLogs(String prefix, PrintWriter pw)553 public void dumpLogs(String prefix, PrintWriter pw) { 554 pw.println(prefix + "RotationButtonController:"); 555 556 pw.println(String.format( 557 "%s\tmIsRecentsAnimationRunning=%b", prefix, mIsRecentsAnimationRunning)); 558 pw.println(String.format("%s\tmHomeRotationEnabled=%b", prefix, mHomeRotationEnabled)); 559 pw.println(String.format( 560 "%s\tmLastRotationSuggestion=%d", prefix, mLastRotationSuggestion)); 561 pw.println(String.format( 562 "%s\tmPendingRotationSuggestion=%b", prefix, mPendingRotationSuggestion)); 563 pw.println(String.format( 564 "%s\tmHoveringRotationSuggestion=%b", prefix, mHoveringRotationSuggestion)); 565 pw.println(String.format("%s\tmListenersRegistered=%b", prefix, mListenersRegistered)); 566 pw.println(String.format( 567 "%s\tmIsNavigationBarShowing=%b", prefix, mIsNavigationBarShowing)); 568 pw.println(String.format("%s\tmBehavior=%d", prefix, mBehavior)); 569 pw.println(String.format( 570 "%s\tmSkipOverrideUserLockPrefsOnce=%b", prefix, mSkipOverrideUserLockPrefsOnce)); 571 pw.println(String.format( 572 "%s\tmLightIconColor=0x%s", prefix, Integer.toHexString(mLightIconColor))); 573 pw.println(String.format( 574 "%s\tmDarkIconColor=0x%s", prefix, Integer.toHexString(mDarkIconColor))); 575 } 576 getRotationButton()577 public RotationButton getRotationButton() { 578 return mRotationButton; 579 } 580 onRotateSuggestionClick(View v)581 private void onRotateSuggestionClick(View v) { 582 mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED); 583 incrementNumAcceptedRotationSuggestionsIfNeeded(); 584 setRotationLockedAtAngle( 585 RotationPolicyUtil.isRotationLocked(mContext), mLastRotationSuggestion, 586 /* caller= */ "RotationButtonController#onRotateSuggestionClick"); 587 Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion); 588 v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 589 } 590 onRotateSuggestionHover(View v, MotionEvent event)591 private boolean onRotateSuggestionHover(View v, MotionEvent event) { 592 final int action = event.getActionMasked(); 593 mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER) 594 || (action == MotionEvent.ACTION_HOVER_MOVE); 595 rescheduleRotationTimeout(true /* reasonHover */); 596 return false; // Must return false so a11y hover events are dispatched correctly. 597 } 598 onRotationSuggestionsDisabled()599 private void onRotationSuggestionsDisabled() { 600 // Immediately hide the rotate button and clear any planned removal 601 setRotateSuggestionButtonState(false /* visible */, true /* force */); 602 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 603 } 604 showAndLogRotationSuggestion()605 private void showAndLogRotationSuggestion() { 606 setRotateSuggestionButtonState(true /* visible */); 607 rescheduleRotationTimeout(false /* reasonHover */); 608 mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN); 609 } 610 611 /** 612 * Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to 613 * avoid losing original user rotation when display rotation is changed by entering the fixed 614 * orientation overview. 615 */ setSkipOverrideUserLockPrefsOnce()616 public void setSkipOverrideUserLockPrefsOnce() { 617 // If live-tile is enabled (recents animation keeps running in overview), there is no 618 // activity switch so the display rotation is not changed, then it is no need to skip. 619 mSkipOverrideUserLockPrefsOnce = !mIsRecentsAnimationRunning; 620 } 621 shouldOverrideUserLockPrefs(final int rotation)622 private boolean shouldOverrideUserLockPrefs(final int rotation) { 623 if (mSkipOverrideUserLockPrefsOnce) { 624 mSkipOverrideUserLockPrefsOnce = false; 625 return false; 626 } 627 // Only override user prefs when returning to the natural rotation (normally portrait). 628 // Don't let apps that force landscape or 180 alter user lock. 629 return rotation == NATURAL_ROTATION; 630 } 631 rescheduleRotationTimeout(final boolean reasonHover)632 private void rescheduleRotationTimeout(final boolean reasonHover) { 633 // May be called due to a new rotation proposal or a change in hover state 634 if (reasonHover) { 635 // Don't reschedule if a hide animator is running 636 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; 637 // Don't reschedule if not visible 638 if (!mRotationButton.isVisible()) return; 639 } 640 641 // Stop any pending removal 642 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 643 // Schedule timeout 644 mMainThreadHandler.postDelayed(mRemoveRotationProposal, 645 computeRotationProposalTimeout()); 646 } 647 computeRotationProposalTimeout()648 private int computeRotationProposalTimeout() { 649 return mAccessibilityManager.getRecommendedTimeoutMillis( 650 mHoveringRotationSuggestion ? 16000 : 5000, 651 AccessibilityManager.FLAG_CONTENT_CONTROLS); 652 } 653 isRotateSuggestionIntroduced()654 private boolean isRotateSuggestionIntroduced() { 655 ContentResolver cr = mContext.getContentResolver(); 656 return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0) 657 >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION; 658 } 659 incrementNumAcceptedRotationSuggestionsIfNeeded()660 private void incrementNumAcceptedRotationSuggestionsIfNeeded() { 661 // Get the number of accepted suggestions 662 ContentResolver cr = mContext.getContentResolver(); 663 final int numSuggestions = Settings.Secure.getInt(cr, 664 Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0); 665 666 // Increment the number of accepted suggestions only if it would change intro mode 667 if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) { 668 Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 669 numSuggestions + 1); 670 } 671 } 672 673 private class TaskStackListenerImpl implements TaskStackChangeListener { 674 // Invalidate any rotation suggestion on task change or activity orientation change 675 // Note: all callbacks happen on main thread 676 677 @Override onTaskStackChanged()678 public void onTaskStackChanged() { 679 setRotateSuggestionButtonState(false /* visible */); 680 } 681 682 @Override onTaskRemoved(int taskId)683 public void onTaskRemoved(int taskId) { 684 setRotateSuggestionButtonState(false /* visible */); 685 } 686 687 @Override onTaskMovedToFront(int taskId)688 public void onTaskMovedToFront(int taskId) { 689 setRotateSuggestionButtonState(false /* visible */); 690 } 691 692 @Override onActivityRequestedOrientationChanged(int taskId, int requestedOrientation)693 public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { 694 mBgExecutor.execute(() -> { 695 // Only hide the icon if the top task changes its requestedOrientation Launcher can 696 // alter its requestedOrientation while it's not on top, don't hide on this 697 Optional.ofNullable(ActivityManagerWrapper.getInstance()) 698 .map(ActivityManagerWrapper::getRunningTask) 699 .ifPresent(a -> { 700 if (a.id == taskId) { 701 mMainThreadHandler.post(() -> 702 setRotateSuggestionButtonState(false /* visible */)); 703 } 704 }); 705 }); 706 } 707 } 708 709 enum RotationButtonEvent implements UiEventLogger.UiEventEnum { 710 @UiEvent(doc = "The rotation button was shown") 711 ROTATION_SUGGESTION_SHOWN(206), 712 @UiEvent(doc = "The rotation button was clicked") 713 ROTATION_SUGGESTION_ACCEPTED(207); 714 715 private final int mId; 716 RotationButtonEvent(int id)717 RotationButtonEvent(int id) { 718 mId = id; 719 } 720 721 @Override getId()722 public int getId() { 723 return mId; 724 } 725 } 726 } 727