1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.navigationbar; 18 19 import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ObjectAnimator; 24 import android.annotation.ColorInt; 25 import android.annotation.DrawableRes; 26 import android.app.StatusBarManager; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.RemoteException; 32 import android.provider.Settings; 33 import android.util.Log; 34 import android.view.IRotationWatcher.Stub; 35 import android.view.MotionEvent; 36 import android.view.Surface; 37 import android.view.View; 38 import android.view.WindowInsetsController; 39 import android.view.WindowInsetsController.Behavior; 40 import android.view.WindowManagerGlobal; 41 import android.view.accessibility.AccessibilityManager; 42 43 import com.android.internal.logging.UiEvent; 44 import com.android.internal.logging.UiEventLogger; 45 import com.android.internal.logging.UiEventLoggerImpl; 46 import com.android.systemui.Dependency; 47 import com.android.systemui.R; 48 import com.android.systemui.animation.Interpolators; 49 import com.android.systemui.navigationbar.buttons.KeyButtonDrawable; 50 import com.android.systemui.shared.system.ActivityManagerWrapper; 51 import com.android.systemui.shared.system.TaskStackChangeListener; 52 import com.android.systemui.shared.system.TaskStackChangeListeners; 53 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; 54 import com.android.systemui.statusbar.policy.RotationLockController; 55 56 import java.util.Optional; 57 import java.util.function.Consumer; 58 59 /** Contains logic that deals with showing a rotate suggestion button with animation. */ 60 public class RotationButtonController { 61 62 private static final String TAG = "StatusBar/RotationButtonController"; 63 private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100; 64 private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000; 65 66 private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3; 67 68 private final Context mContext; 69 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 70 private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); 71 private final ViewRippler mViewRippler = new ViewRippler(); 72 private RotationButton mRotationButton; 73 74 private boolean mIsRecentsAnimationRunning; 75 private boolean mHomeRotationEnabled; 76 private int mLastRotationSuggestion; 77 private boolean mPendingRotationSuggestion; 78 private boolean mHoveringRotationSuggestion; 79 private RotationLockController mRotationLockController; 80 private AccessibilityManagerWrapper mAccessibilityManagerWrapper; 81 private TaskStackListenerImpl mTaskStackListener; 82 private Consumer<Integer> mRotWatcherListener; 83 private boolean mListenersRegistered = false; 84 private boolean mIsNavigationBarShowing; 85 private @Behavior int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT; 86 private boolean mSkipOverrideUserLockPrefsOnce; 87 private int mLightIconColor; 88 private int mDarkIconColor; 89 private int mIconResId = R.drawable.ic_sysbar_rotate_button_ccw_start_90; 90 91 private final Runnable mRemoveRotationProposal = 92 () -> setRotateSuggestionButtonState(false /* visible */); 93 private final Runnable mCancelPendingRotationProposal = 94 () -> mPendingRotationSuggestion = false; 95 private Animator mRotateHideAnimator; 96 97 private final Stub mRotationWatcher = new Stub() { 98 @Override 99 public void onRotationChanged(final int rotation) throws RemoteException { 100 // We need this to be scheduled as early as possible to beat the redrawing of 101 // window in response to the orientation change. 102 mMainThreadHandler.postAtFrontOfQueue(() -> { 103 // If the screen rotation changes while locked, potentially update lock to flow with 104 // new screen rotation and hide any showing suggestions. 105 if (mRotationLockController.isRotationLocked()) { 106 if (shouldOverrideUserLockPrefs(rotation)) { 107 setRotationLockedAtAngle(rotation); 108 } 109 setRotateSuggestionButtonState(false /* visible */, true /* hideImmediately */); 110 } 111 112 if (mRotWatcherListener != null) { 113 mRotWatcherListener.accept(rotation); 114 } 115 }); 116 } 117 }; 118 119 /** 120 * Determines if rotation suggestions disabled2 flag exists in flag 121 * @param disable2Flags see if rotation suggestion flag exists in this flag 122 * @return whether flag exists 123 */ hasDisable2RotateSuggestionFlag(int disable2Flags)124 static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) { 125 return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0; 126 } 127 RotationButtonController(Context context, @ColorInt int lightIconColor, @ColorInt int darkIconColor)128 RotationButtonController(Context context, @ColorInt int lightIconColor, 129 @ColorInt int darkIconColor) { 130 mContext = context; 131 mLightIconColor = lightIconColor; 132 mDarkIconColor = darkIconColor; 133 134 mIsNavigationBarShowing = true; 135 mRotationLockController = Dependency.get(RotationLockController.class); 136 mAccessibilityManagerWrapper = Dependency.get(AccessibilityManagerWrapper.class); 137 mTaskStackListener = new TaskStackListenerImpl(); 138 } 139 setRotationButton(RotationButton rotationButton, Consumer<Boolean> visibilityChangedCallback)140 void setRotationButton(RotationButton rotationButton, 141 Consumer<Boolean> visibilityChangedCallback) { 142 mRotationButton = rotationButton; 143 mRotationButton.setRotationButtonController(this); 144 mRotationButton.setOnClickListener(this::onRotateSuggestionClick); 145 mRotationButton.setOnHoverListener(this::onRotateSuggestionHover); 146 mRotationButton.setVisibilityChangedCallback(visibilityChangedCallback); 147 } 148 registerListeners()149 void registerListeners() { 150 if (mListenersRegistered) { 151 return; 152 } 153 154 mListenersRegistered = true; 155 try { 156 WindowManagerGlobal.getWindowManagerService() 157 .watchRotation(mRotationWatcher, mContext.getDisplay().getDisplayId()); 158 } catch (IllegalArgumentException e) { 159 mListenersRegistered = false; 160 Log.w(TAG, "RegisterListeners for the display failed"); 161 } catch (RemoteException e) { 162 throw e.rethrowFromSystemServer(); 163 } 164 165 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 166 } 167 unregisterListeners()168 void unregisterListeners() { 169 if (!mListenersRegistered) { 170 return; 171 } 172 173 mListenersRegistered = false; 174 try { 175 WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher); 176 } catch (RemoteException e) { 177 throw e.rethrowFromSystemServer(); 178 } 179 180 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 181 } 182 addRotationCallback(Consumer<Integer> watcher)183 void addRotationCallback(Consumer<Integer> watcher) { 184 mRotWatcherListener = watcher; 185 } 186 setRotationLockedAtAngle(int rotationSuggestion)187 void setRotationLockedAtAngle(int rotationSuggestion) { 188 mRotationLockController.setRotationLockedAtAngle(true /* locked */, rotationSuggestion); 189 } 190 isRotationLocked()191 public boolean isRotationLocked() { 192 return mRotationLockController.isRotationLocked(); 193 } 194 setRotateSuggestionButtonState(boolean visible)195 void setRotateSuggestionButtonState(boolean visible) { 196 setRotateSuggestionButtonState(visible, false /* hideImmediately */); 197 } 198 199 /** 200 * Change the visibility of rotate suggestion button. If {@code hideImmediately} is true, 201 * it doesn't wait until the completion of the running animation. 202 */ setRotateSuggestionButtonState(final boolean visible, final boolean hideImmediately)203 void setRotateSuggestionButtonState(final boolean visible, final boolean hideImmediately) { 204 // At any point the the button can become invisible because an a11y service became active. 205 // Similarly, a call to make the button visible may be rejected because an a11y service is 206 // active. Must account for this. 207 // Rerun a show animation to indicate change but don't rerun a hide animation 208 if (!visible && !mRotationButton.isVisible()) return; 209 210 final View view = mRotationButton.getCurrentView(); 211 if (view == null) return; 212 213 final KeyButtonDrawable currentDrawable = mRotationButton.getImageDrawable(); 214 if (currentDrawable == null) return; 215 216 // Clear any pending suggestion flag as it has either been nullified or is being shown 217 mPendingRotationSuggestion = false; 218 mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal); 219 220 // Handle the visibility change and animation 221 if (visible) { // Appear and change (cannot force) 222 // Stop and clear any currently running hide animations 223 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { 224 mRotateHideAnimator.cancel(); 225 } 226 mRotateHideAnimator = null; 227 228 // Reset the alpha if any has changed due to hide animation 229 view.setAlpha(1f); 230 231 // Run the rotate icon's animation if it has one 232 if (currentDrawable.canAnimate()) { 233 currentDrawable.resetAnimation(); 234 currentDrawable.startAnimation(); 235 } 236 237 if (!isRotateSuggestionIntroduced()) mViewRippler.start(view); 238 239 // Set visibility unless a11y service is active. 240 mRotationButton.show(); 241 } else { // Hide 242 mViewRippler.stop(); // Prevent any pending ripples, force hide or not 243 244 if (hideImmediately) { 245 // If a hide animator is running stop it and make invisible 246 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { 247 mRotateHideAnimator.pause(); 248 } 249 mRotationButton.hide(); 250 return; 251 } 252 253 // Don't start any new hide animations if one is running 254 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; 255 256 ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f); 257 fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS); 258 fadeOut.setInterpolator(Interpolators.LINEAR); 259 fadeOut.addListener(new AnimatorListenerAdapter() { 260 @Override 261 public void onAnimationEnd(Animator animation) { 262 mRotationButton.hide(); 263 } 264 }); 265 266 mRotateHideAnimator = fadeOut; 267 fadeOut.start(); 268 } 269 } 270 setRecentsAnimationRunning(boolean running)271 void setRecentsAnimationRunning(boolean running) { 272 mIsRecentsAnimationRunning = running; 273 updateRotationButtonStateInOverview(); 274 } 275 setHomeRotationEnabled(boolean enabled)276 void setHomeRotationEnabled(boolean enabled) { 277 mHomeRotationEnabled = enabled; 278 updateRotationButtonStateInOverview(); 279 } 280 updateRotationButtonStateInOverview()281 private void updateRotationButtonStateInOverview() { 282 if (mIsRecentsAnimationRunning && !mHomeRotationEnabled) { 283 setRotateSuggestionButtonState(false, true /* hideImmediately */ ); 284 } 285 } 286 setDarkIntensity(float darkIntensity)287 void setDarkIntensity(float darkIntensity) { 288 mRotationButton.setDarkIntensity(darkIntensity); 289 } 290 onRotationProposal(int rotation, int windowRotation, boolean isValid)291 void onRotationProposal(int rotation, int windowRotation, boolean isValid) { 292 if (!mRotationButton.acceptRotationProposal() || (!mHomeRotationEnabled 293 && mIsRecentsAnimationRunning)) { 294 return; 295 } 296 297 // This method will be called on rotation suggestion changes even if the proposed rotation 298 // is not valid for the top app. Use invalid rotation choices as a signal to remove the 299 // rotate button if shown. 300 if (!isValid) { 301 setRotateSuggestionButtonState(false /* visible */); 302 return; 303 } 304 305 // If window rotation matches suggested rotation, remove any current suggestions 306 if (rotation == windowRotation) { 307 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 308 setRotateSuggestionButtonState(false /* visible */); 309 return; 310 } 311 312 // Prepare to show the navbar icon by updating the icon style to change anim params 313 mLastRotationSuggestion = rotation; // Remember rotation for click 314 final boolean rotationCCW = isRotationAnimationCCW(windowRotation, rotation); 315 if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) { 316 mIconResId = rotationCCW 317 ? R.drawable.ic_sysbar_rotate_button_ccw_start_90 318 : R.drawable.ic_sysbar_rotate_button_cw_start_90; 319 } else { // 90 or 270 320 mIconResId = rotationCCW 321 ? R.drawable.ic_sysbar_rotate_button_ccw_start_0 322 : R.drawable.ic_sysbar_rotate_button_ccw_start_0; 323 } 324 mRotationButton.updateIcon(mLightIconColor, mDarkIconColor); 325 326 if (canShowRotationButton()) { 327 // The navbar is visible / it's in visual immersive mode, so show the icon right away 328 showAndLogRotationSuggestion(); 329 } else { 330 // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become 331 // visible given some time limit. 332 mPendingRotationSuggestion = true; 333 mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal); 334 mMainThreadHandler.postDelayed(mCancelPendingRotationProposal, 335 NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS); 336 } 337 } 338 onDisable2FlagChanged(int state2)339 void onDisable2FlagChanged(int state2) { 340 final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2); 341 if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled(); 342 } 343 onNavigationBarWindowVisibilityChange(boolean showing)344 void onNavigationBarWindowVisibilityChange(boolean showing) { 345 if (mIsNavigationBarShowing != showing) { 346 mIsNavigationBarShowing = showing; 347 showPendingRotationButtonIfNeeded(); 348 } 349 } 350 onBehaviorChanged(@ehavior int behavior)351 void onBehaviorChanged(@Behavior int behavior) { 352 if (mBehavior != behavior) { 353 mBehavior = behavior; 354 showPendingRotationButtonIfNeeded(); 355 } 356 } 357 showPendingRotationButtonIfNeeded()358 private void showPendingRotationButtonIfNeeded() { 359 if (canShowRotationButton() && mPendingRotationSuggestion) { 360 showAndLogRotationSuggestion(); 361 } 362 } 363 364 /** Return true when either the nav bar is visible or it's in visual immersive mode. */ canShowRotationButton()365 private boolean canShowRotationButton() { 366 return mIsNavigationBarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT; 367 } 368 getContext()369 public Context getContext() { 370 return mContext; 371 } 372 getRotationButton()373 RotationButton getRotationButton() { 374 return mRotationButton; 375 } 376 getIconResId()377 public @DrawableRes int getIconResId() { 378 return mIconResId; 379 } 380 getLightIconColor()381 public @ColorInt int getLightIconColor() { 382 return mLightIconColor; 383 } 384 getDarkIconColor()385 public @ColorInt int getDarkIconColor() { 386 return mDarkIconColor; 387 } 388 onRotateSuggestionClick(View v)389 private void onRotateSuggestionClick(View v) { 390 mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED); 391 incrementNumAcceptedRotationSuggestionsIfNeeded(); 392 setRotationLockedAtAngle(mLastRotationSuggestion); 393 } 394 onRotateSuggestionHover(View v, MotionEvent event)395 private boolean onRotateSuggestionHover(View v, MotionEvent event) { 396 final int action = event.getActionMasked(); 397 mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER) 398 || (action == MotionEvent.ACTION_HOVER_MOVE); 399 rescheduleRotationTimeout(true /* reasonHover */); 400 return false; // Must return false so a11y hover events are dispatched correctly. 401 } 402 onRotationSuggestionsDisabled()403 private void onRotationSuggestionsDisabled() { 404 // Immediately hide the rotate button and clear any planned removal 405 setRotateSuggestionButtonState(false /* visible */, true /* force */); 406 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 407 } 408 showAndLogRotationSuggestion()409 private void showAndLogRotationSuggestion() { 410 setRotateSuggestionButtonState(true /* visible */); 411 rescheduleRotationTimeout(false /* reasonHover */); 412 mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN); 413 } 414 415 /** 416 * Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to 417 * avoid losing original user rotation when display rotation is changed by entering the fixed 418 * orientation overview. 419 */ setSkipOverrideUserLockPrefsOnce()420 void setSkipOverrideUserLockPrefsOnce() { 421 mSkipOverrideUserLockPrefsOnce = true; 422 } 423 shouldOverrideUserLockPrefs(final int rotation)424 private boolean shouldOverrideUserLockPrefs(final int rotation) { 425 if (mSkipOverrideUserLockPrefsOnce) { 426 mSkipOverrideUserLockPrefsOnce = false; 427 return false; 428 } 429 // Only override user prefs when returning to the natural rotation (normally portrait). 430 // Don't let apps that force landscape or 180 alter user lock. 431 return rotation == NATURAL_ROTATION; 432 } 433 isRotationAnimationCCW(int from, int to)434 private boolean isRotationAnimationCCW(int from, int to) { 435 // All 180deg WM rotation animations are CCW, match that 436 if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false; 437 if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW 438 if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true; 439 if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true; 440 if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false; 441 if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW 442 if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW 443 if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true; 444 if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false; 445 if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false; 446 if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW 447 if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true; 448 return false; // Default 449 } 450 rescheduleRotationTimeout(final boolean reasonHover)451 private void rescheduleRotationTimeout(final boolean reasonHover) { 452 // May be called due to a new rotation proposal or a change in hover state 453 if (reasonHover) { 454 // Don't reschedule if a hide animator is running 455 if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; 456 // Don't reschedule if not visible 457 if (!mRotationButton.isVisible()) return; 458 } 459 460 // Stop any pending removal 461 mMainThreadHandler.removeCallbacks(mRemoveRotationProposal); 462 // Schedule timeout 463 mMainThreadHandler.postDelayed(mRemoveRotationProposal, 464 computeRotationProposalTimeout()); 465 } 466 computeRotationProposalTimeout()467 private int computeRotationProposalTimeout() { 468 return mAccessibilityManagerWrapper.getRecommendedTimeoutMillis( 469 mHoveringRotationSuggestion ? 16000 : 5000, 470 AccessibilityManager.FLAG_CONTENT_CONTROLS); 471 } 472 isRotateSuggestionIntroduced()473 private boolean isRotateSuggestionIntroduced() { 474 ContentResolver cr = mContext.getContentResolver(); 475 return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0) 476 >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION; 477 } 478 incrementNumAcceptedRotationSuggestionsIfNeeded()479 private void incrementNumAcceptedRotationSuggestionsIfNeeded() { 480 // Get the number of accepted suggestions 481 ContentResolver cr = mContext.getContentResolver(); 482 final int numSuggestions = Settings.Secure.getInt(cr, 483 Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0); 484 485 // Increment the number of accepted suggestions only if it would change intro mode 486 if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) { 487 Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 488 numSuggestions + 1); 489 } 490 } 491 492 private class TaskStackListenerImpl extends TaskStackChangeListener { 493 // Invalidate any rotation suggestion on task change or activity orientation change 494 // Note: all callbacks happen on main thread 495 496 @Override onTaskStackChanged()497 public void onTaskStackChanged() { 498 setRotateSuggestionButtonState(false /* visible */); 499 } 500 501 @Override onTaskRemoved(int taskId)502 public void onTaskRemoved(int taskId) { 503 setRotateSuggestionButtonState(false /* visible */); 504 } 505 506 @Override onTaskMovedToFront(int taskId)507 public void onTaskMovedToFront(int taskId) { 508 setRotateSuggestionButtonState(false /* visible */); 509 } 510 511 @Override onActivityRequestedOrientationChanged(int taskId, int requestedOrientation)512 public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { 513 // Only hide the icon if the top task changes its requestedOrientation 514 // Launcher can alter its requestedOrientation while it's not on top, don't hide on this 515 Optional.ofNullable(ActivityManagerWrapper.getInstance()) 516 .map(ActivityManagerWrapper::getRunningTask) 517 .ifPresent(a -> { 518 if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */); 519 }); 520 } 521 } 522 523 private class ViewRippler { 524 private static final int RIPPLE_OFFSET_MS = 50; 525 private static final int RIPPLE_INTERVAL_MS = 2000; 526 private View mRoot; 527 start(View root)528 public void start(View root) { 529 stop(); // Stop any pending ripple animations 530 531 mRoot = root; 532 533 // Schedule pending ripples, offset the 1st to avoid problems with visibility change 534 mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS); 535 mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS); 536 mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS); 537 mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS); 538 mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS); 539 } 540 stop()541 public void stop() { 542 if (mRoot != null) mRoot.removeCallbacks(mRipple); 543 } 544 545 private final Runnable mRipple = new Runnable() { 546 @Override 547 public void run() { // Cause the ripple to fire via false presses 548 if (!mRoot.isAttachedToWindow()) return; 549 mRoot.setPressed(true /* pressed */); 550 mRoot.setPressed(false /* pressed */); 551 } 552 }; 553 } 554 555 enum RotationButtonEvent implements UiEventLogger.UiEventEnum { 556 @UiEvent(doc = "The rotation button was shown") 557 ROTATION_SUGGESTION_SHOWN(206), 558 @UiEvent(doc = "The rotation button was clicked") 559 ROTATION_SUGGESTION_ACCEPTED(207); 560 561 private final int mId; RotationButtonEvent(int id)562 RotationButtonEvent(int id) { 563 mId = id; 564 } getId()565 @Override public int getId() { 566 return mId; 567 } 568 } 569 } 570