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