• 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.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