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