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