• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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