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