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