NavigationBarFragment.java revision b06ae3bf131b0bd0ded398d3a06974a6a9d78a50
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.systemui.statusbar.phone;
16
17import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
18import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
19import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
20import static android.app.StatusBarManager.windowStateToString;
21
22import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
23
24import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType;
25import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT;
26import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE;
27import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions;
28import static com.android.systemui.OverviewProxyService.OverviewProxyListener;
29
30import android.accessibilityservice.AccessibilityServiceInfo;
31import android.animation.Animator;
32import android.animation.AnimatorListenerAdapter;
33import android.animation.ObjectAnimator;
34import android.annotation.IdRes;
35import android.annotation.Nullable;
36import android.app.ActivityManager;
37import android.app.ActivityManagerNative;
38import android.app.Fragment;
39import android.app.IActivityManager;
40import android.app.StatusBarManager;
41import android.content.BroadcastReceiver;
42import android.content.ContentResolver;
43import android.content.Context;
44import android.content.Intent;
45import android.content.IntentFilter;
46import android.content.res.Configuration;
47import android.database.ContentObserver;
48import android.graphics.PixelFormat;
49import android.graphics.Rect;
50import android.graphics.drawable.AnimatedVectorDrawable;
51import android.inputmethodservice.InputMethodService;
52import android.os.Binder;
53import android.os.Bundle;
54import android.os.Handler;
55import android.os.IBinder;
56import android.os.Message;
57import android.os.RemoteException;
58import android.os.UserHandle;
59import android.provider.Settings;
60import android.support.annotation.VisibleForTesting;
61import android.telecom.TelecomManager;
62import android.text.TextUtils;
63import android.util.Log;
64import android.view.IRotationWatcher.Stub;
65import android.view.KeyEvent;
66import android.view.LayoutInflater;
67import android.view.MotionEvent;
68import android.view.Surface;
69import android.view.View;
70import android.view.ViewGroup;
71import android.view.WindowManager;
72import android.view.WindowManager.LayoutParams;
73import android.view.WindowManagerGlobal;
74import android.view.accessibility.AccessibilityEvent;
75import android.view.accessibility.AccessibilityManager;
76import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
77
78import com.android.internal.logging.MetricsLogger;
79import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
80import com.android.internal.util.LatencyTracker;
81import com.android.systemui.Dependency;
82import com.android.systemui.Interpolators;
83import com.android.systemui.OverviewProxyService;
84import com.android.systemui.R;
85import com.android.systemui.SysUiServiceProvider;
86import com.android.systemui.assist.AssistManager;
87import com.android.systemui.fragments.FragmentHostManager;
88import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
89import com.android.systemui.recents.Recents;
90import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
91import com.android.systemui.shared.system.ActivityManagerWrapper;
92import com.android.systemui.shared.system.WindowManagerWrapper;
93import com.android.systemui.stackdivider.Divider;
94import com.android.systemui.statusbar.CommandQueue;
95import com.android.systemui.statusbar.CommandQueue.Callbacks;
96import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
97import com.android.systemui.statusbar.policy.KeyButtonDrawable;
98import com.android.systemui.statusbar.policy.KeyButtonView;
99import com.android.systemui.statusbar.policy.RotationLockController;
100import com.android.systemui.statusbar.stack.StackStateAnimator;
101
102import java.io.FileDescriptor;
103import java.io.PrintWriter;
104import java.util.List;
105import java.util.Locale;
106
107/**
108 * Fragment containing the NavigationBarFragment. Contains logic for what happens
109 * on clicks and view states of the nav bar.
110 */
111public class NavigationBarFragment extends Fragment implements Callbacks {
112
113    public static final String TAG = "NavigationBar";
114    private static final boolean DEBUG = false;
115    private static final String EXTRA_DISABLE_STATE = "disabled_state";
116
117    private final static int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
118    private final static int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
119
120    private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
121
122    /** Allow some time inbetween the long press for back and recents. */
123    private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200;
124
125    protected NavigationBarView mNavigationBarView = null;
126    protected AssistManager mAssistManager;
127
128    private int mNavigationBarWindowState = WINDOW_STATE_SHOWING;
129
130    private int mNavigationIconHints = 0;
131    private int mNavigationBarMode;
132    private boolean mAccessibilityFeedbackEnabled;
133    private AccessibilityManager mAccessibilityManager;
134    private MagnificationContentObserver mMagnificationObserver;
135    private ContentResolver mContentResolver;
136    private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
137
138    private int mDisabledFlags1;
139    private StatusBar mStatusBar;
140    private Recents mRecents;
141    private Divider mDivider;
142    private WindowManager mWindowManager;
143    private CommandQueue mCommandQueue;
144    private long mLastLockToAppLongPress;
145
146    private Locale mLocale;
147    private int mLayoutDirection;
148
149    private int mSystemUiVisibility;
150    private LightBarController mLightBarController;
151
152    private OverviewProxyService mOverviewProxyService;
153
154    public boolean mHomeBlockedThisTouch;
155
156    private int mLastRotationSuggestion;
157    private boolean mPendingRotationSuggestion;
158    private boolean mHoveringRotationSuggestion;
159    private RotationLockController mRotationLockController;
160    private TaskStackListenerImpl mTaskStackListener;
161
162    private final Runnable mRemoveRotationProposal = () -> setRotateSuggestionButtonState(false);
163    private final Runnable mCancelPendingRotationProposal =
164            () -> mPendingRotationSuggestion = false;
165    private Animator mRotateHideAnimator;
166    private ViewRippler mViewRippler = new ViewRippler();
167
168    private final OverviewProxyListener mOverviewProxyListener = new OverviewProxyListener() {
169        @Override
170        public void onConnectionChanged(boolean isConnected) {
171            mNavigationBarView.onOverviewProxyConnectionChanged(isConnected);
172            updateScreenPinningGestures();
173            WindowManagerWrapper.getInstance()
174                    .setNavBarVirtualKeyHapticFeedbackEnabled(!isConnected);
175        }
176
177        @Override
178        public void onQuickStepStarted() {
179            mNavigationBarView.onQuickStepStarted();
180
181            // Use navbar dragging as a signal to hide the rotate button
182            setRotateSuggestionButtonState(false);
183        }
184
185        @Override
186        public void onInteractionFlagsChanged(@InteractionType int flags) {
187            mNavigationBarView.updateStates();
188        }
189    };
190
191    // ----- Fragment Lifecycle Callbacks -----
192
193    @Override
194    public void onCreate(@Nullable Bundle savedInstanceState) {
195        super.onCreate(savedInstanceState);
196        mCommandQueue = SysUiServiceProvider.getComponent(getContext(), CommandQueue.class);
197        mCommandQueue.addCallbacks(this);
198        mStatusBar = SysUiServiceProvider.getComponent(getContext(), StatusBar.class);
199        mRecents = SysUiServiceProvider.getComponent(getContext(), Recents.class);
200        mDivider = SysUiServiceProvider.getComponent(getContext(), Divider.class);
201        mWindowManager = getContext().getSystemService(WindowManager.class);
202        mAccessibilityManager = getContext().getSystemService(AccessibilityManager.class);
203        Dependency.get(AccessibilityManagerWrapper.class).addCallback(
204                mAccessibilityListener);
205        mContentResolver = getContext().getContentResolver();
206        mMagnificationObserver = new MagnificationContentObserver(
207                getContext().getMainThreadHandler());
208        mContentResolver.registerContentObserver(Settings.Secure.getUriFor(
209                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED), false,
210                mMagnificationObserver, UserHandle.USER_ALL);
211
212        if (savedInstanceState != null) {
213            mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0);
214        }
215        mAssistManager = Dependency.get(AssistManager.class);
216        mOverviewProxyService = Dependency.get(OverviewProxyService.class);
217
218        try {
219            WindowManagerGlobal.getWindowManagerService()
220                    .watchRotation(mRotationWatcher, getContext().getDisplay().getDisplayId());
221        } catch (RemoteException e) {
222            throw e.rethrowFromSystemServer();
223        }
224
225        mRotationLockController = Dependency.get(RotationLockController.class);
226
227        // Reset user rotation pref to match that of the WindowManager if starting in locked mode
228        // This will automatically happen when switching from auto-rotate to locked mode
229        if (mRotationLockController.isRotationLocked()) {
230            final int winRotation = mWindowManager.getDefaultDisplay().getRotation();
231            mRotationLockController.setRotationLockedAtAngle(true, winRotation);
232        }
233
234        // Register the task stack listener
235        mTaskStackListener = new TaskStackListenerImpl();
236        ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
237    }
238
239    @Override
240    public void onDestroy() {
241        super.onDestroy();
242        mCommandQueue.removeCallbacks(this);
243        Dependency.get(AccessibilityManagerWrapper.class).removeCallback(
244                mAccessibilityListener);
245        mContentResolver.unregisterContentObserver(mMagnificationObserver);
246        try {
247            WindowManagerGlobal.getWindowManagerService()
248                    .removeRotationWatcher(mRotationWatcher);
249        } catch (RemoteException e) {
250            throw e.rethrowFromSystemServer();
251        }
252
253        // Unregister the task stack listener
254        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
255    }
256
257    @Override
258    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
259            Bundle savedInstanceState) {
260        return inflater.inflate(R.layout.navigation_bar, container, false);
261    }
262
263    @Override
264    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
265        super.onViewCreated(view, savedInstanceState);
266        mNavigationBarView = (NavigationBarView) view;
267
268        mNavigationBarView.setDisabledFlags(mDisabledFlags1);
269        mNavigationBarView.setComponents(mRecents, mDivider, mStatusBar.getPanel());
270        mNavigationBarView.setOnVerticalChangedListener(this::onVerticalChanged);
271        mNavigationBarView.setOnTouchListener(this::onNavigationTouch);
272        if (savedInstanceState != null) {
273            mNavigationBarView.getLightTransitionsController().restoreState(savedInstanceState);
274        }
275
276        prepareNavigationBarView();
277        checkNavBarModes();
278
279        IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
280        filter.addAction(Intent.ACTION_SCREEN_ON);
281        getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null);
282        notifyNavigationBarScreenOn();
283        mOverviewProxyService.addCallback(mOverviewProxyListener);
284    }
285
286    @Override
287    public void onDestroyView() {
288        super.onDestroyView();
289        mNavigationBarView.getLightTransitionsController().destroy(getContext());
290        mOverviewProxyService.removeCallback(mOverviewProxyListener);
291        getContext().unregisterReceiver(mBroadcastReceiver);
292    }
293
294    @Override
295    public void onSaveInstanceState(Bundle outState) {
296        super.onSaveInstanceState(outState);
297        outState.putInt(EXTRA_DISABLE_STATE, mDisabledFlags1);
298        if (mNavigationBarView != null) {
299            mNavigationBarView.getLightTransitionsController().saveState(outState);
300        }
301    }
302
303    @Override
304    public void onConfigurationChanged(Configuration newConfig) {
305        super.onConfigurationChanged(newConfig);
306        final Locale locale = getContext().getResources().getConfiguration().locale;
307        final int ld = TextUtils.getLayoutDirectionFromLocale(locale);
308        if (!locale.equals(mLocale) || ld != mLayoutDirection) {
309            if (DEBUG) {
310                Log.v(TAG, String.format(
311                        "config changed locale/LD: %s (%d) -> %s (%d)", mLocale, mLayoutDirection,
312                        locale, ld));
313            }
314            mLocale = locale;
315            mLayoutDirection = ld;
316            refreshLayout(ld);
317        }
318        repositionNavigationBar();
319    }
320
321    @Override
322    public void dump(String prefix, FileDescriptor fd, PrintWriter pw, String[] args) {
323        if (mNavigationBarView != null) {
324            pw.print("  mNavigationBarWindowState=");
325            pw.println(windowStateToString(mNavigationBarWindowState));
326            pw.print("  mNavigationBarMode=");
327            pw.println(BarTransitions.modeToString(mNavigationBarMode));
328            dumpBarTransitions(pw, "mNavigationBarView", mNavigationBarView.getBarTransitions());
329        }
330
331        pw.print("  mNavigationBarView=");
332        if (mNavigationBarView == null) {
333            pw.println("null");
334        } else {
335            mNavigationBarView.dump(fd, pw, args);
336        }
337    }
338
339    // ----- CommandQueue Callbacks -----
340
341    @Override
342    public void setImeWindowStatus(IBinder token, int vis, int backDisposition,
343            boolean showImeSwitcher) {
344        boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
345        int hints = mNavigationIconHints;
346        if (imeShown && backDisposition != InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS) {
347            hints |= NAVIGATION_HINT_BACK_ALT;
348        } else {
349            hints &= ~NAVIGATION_HINT_BACK_ALT;
350        }
351        if (showImeSwitcher) {
352            hints |= NAVIGATION_HINT_IME_SHOWN;
353        } else {
354            hints &= ~NAVIGATION_HINT_IME_SHOWN;
355        }
356        if (hints == mNavigationIconHints) return;
357
358        mNavigationIconHints = hints;
359
360        if (mNavigationBarView != null) {
361            mNavigationBarView.setNavigationIconHints(hints);
362        }
363        mStatusBar.checkBarModes();
364    }
365
366    @Override
367    public void topAppWindowChanged(boolean showMenu) {
368        if (mNavigationBarView != null) {
369            mNavigationBarView.setMenuVisibility(showMenu);
370        }
371    }
372
373    @Override
374    public void setWindowState(int window, int state) {
375        if (mNavigationBarView != null
376                && window == StatusBarManager.WINDOW_NAVIGATION_BAR
377                && mNavigationBarWindowState != state) {
378            mNavigationBarWindowState = state;
379            if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
380
381            // If the navbar is visible, show the rotate button if there's a pending suggestion
382            if (state == WINDOW_STATE_SHOWING && mPendingRotationSuggestion) {
383                showAndLogRotationSuggestion();
384            }
385        }
386    }
387
388    @Override
389    public void onRotationProposal(final int rotation, boolean isValid) {
390        // This method will be called on rotation suggestion changes even if the proposed rotation
391        // is not valid for the top app. Use invalid rotation choices as a signal to remove the
392        // rotate button if shown.
393        if (!isValid) {
394            setRotateSuggestionButtonState(false);
395            return;
396        }
397
398        // If window rotation matches suggested rotation, remove any current suggestions
399        final int winRotation = mWindowManager.getDefaultDisplay().getRotation();
400        if (rotation == winRotation) {
401            getView().removeCallbacks(mRemoveRotationProposal);
402            setRotateSuggestionButtonState(false);
403            return;
404        }
405
406        // Prepare to show the navbar icon by updating the icon style to change anim params
407        mLastRotationSuggestion = rotation; // Remember rotation for click
408        if (mNavigationBarView != null) {
409            final boolean rotationCCW = isRotationAnimationCCW(winRotation, rotation);
410            int style;
411            if (winRotation == Surface.ROTATION_0 || winRotation == Surface.ROTATION_180) {
412                style = rotationCCW ? R.style.RotateButtonCCWStart90 :
413                        R.style.RotateButtonCWStart90;
414            } else { // 90 or 270
415                style = rotationCCW ? R.style.RotateButtonCCWStart0 :
416                        R.style.RotateButtonCWStart0;
417            }
418            mNavigationBarView.updateRotateSuggestionButtonStyle(style, true);
419        }
420
421        if (mNavigationBarWindowState != WINDOW_STATE_SHOWING) {
422            // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
423            // visible given some time limit.
424            mPendingRotationSuggestion = true;
425            getView().removeCallbacks(mCancelPendingRotationProposal);
426            getView().postDelayed(mCancelPendingRotationProposal,
427                    NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
428
429        } else { // The navbar is visible so show the icon right away
430            showAndLogRotationSuggestion();
431        }
432    }
433
434    private void showAndLogRotationSuggestion() {
435        setRotateSuggestionButtonState(true);
436        rescheduleRotationTimeout(false);
437        mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
438    }
439
440    private boolean isRotationAnimationCCW(int from, int to) {
441        // All 180deg WM rotation animations are CCW, match that
442        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
443        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
444        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
445        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
446        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
447        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
448        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
449        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
450        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
451        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
452        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
453        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
454        return false; // Default
455    }
456
457    public void setRotateSuggestionButtonState(final boolean visible) {
458        setRotateSuggestionButtonState(visible, false);
459    }
460
461    public void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
462        if (mNavigationBarView == null) return;
463
464        // At any point the the button can become invisible because an a11y service became active.
465        // Similarly, a call to make the button visible may be rejected because an a11y service is
466        // active. Must account for this.
467
468        ButtonDispatcher rotBtn = mNavigationBarView.getRotateSuggestionButton();
469        final boolean currentlyVisible = mNavigationBarView.isRotateButtonVisible();
470
471        // Rerun a show animation to indicate change but don't rerun a hide animation
472        if (!visible && !currentlyVisible) return;
473
474        View view = rotBtn.getCurrentView();
475        if (view == null) return;
476
477        KeyButtonDrawable kbd = rotBtn.getImageDrawable();
478        if (kbd == null) return;
479
480        // The KBD and AVD is recreated every new valid suggestion because of style changes.
481        AnimatedVectorDrawable animIcon = null;
482        if (kbd.getDrawable(0) instanceof AnimatedVectorDrawable) {
483            animIcon = (AnimatedVectorDrawable) kbd.getDrawable(0);
484        }
485
486        // Clear any pending suggestion flag as it has either been nullified or is being shown
487        mPendingRotationSuggestion = false;
488        getView().removeCallbacks(mCancelPendingRotationProposal);
489
490        // Handle the visibility change and animation
491        if (visible) { // Appear and change (cannot force)
492            // Stop and clear any currently running hide animations
493            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
494                mRotateHideAnimator.cancel();
495            }
496            mRotateHideAnimator = null;
497
498            // Reset the alpha if any has changed due to hide animation
499            view.setAlpha(1f);
500
501            // Run the rotate icon's animation if it has one
502            if (animIcon != null) {
503                animIcon.reset();
504                animIcon.start();
505            }
506
507            if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
508
509            // Set visibility, may fail if a11y service is active.
510            // If invisible, call will stop animation.
511            mNavigationBarView.setRotateButtonVisibility(true);
512
513        } else { // Hide
514
515            mViewRippler.stop(); // Prevent any pending ripples, force hide or not
516
517            if (force) {
518                // If a hide animator is running stop it and make invisible
519                if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
520                    mRotateHideAnimator.pause();
521                }
522                mNavigationBarView.setRotateButtonVisibility(false);
523                return;
524            }
525
526            // Don't start any new hide animations if one is running
527            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
528
529            ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha",
530                    0f);
531            fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
532            fadeOut.setInterpolator(Interpolators.LINEAR);
533            fadeOut.addListener(new AnimatorListenerAdapter() {
534                @Override
535                public void onAnimationEnd(Animator animation) {
536                    mNavigationBarView.setRotateButtonVisibility(false);
537                }
538            });
539
540            mRotateHideAnimator = fadeOut;
541            fadeOut.start();
542        }
543    }
544
545    private void rescheduleRotationTimeout(final boolean reasonHover) {
546        // May be called due to a new rotation proposal or a change in hover state
547        if (reasonHover) {
548            // Don't reschedule if a hide animator is running
549            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
550            // Don't reschedule if not visible
551            if (!mNavigationBarView.isRotateButtonVisible()) return;
552        }
553
554        getView().removeCallbacks(mRemoveRotationProposal); // Stop any pending removal
555        getView().postDelayed(mRemoveRotationProposal,
556                computeRotationProposalTimeout()); // Schedule timeout
557    }
558
559    private int computeRotationProposalTimeout() {
560        if (mAccessibilityFeedbackEnabled) return 20000;
561        if (mHoveringRotationSuggestion) return 16000;
562        return 6000;
563    }
564
565    private boolean isRotateSuggestionIntroduced() {
566        ContentResolver cr = getContext().getContentResolver();
567        return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
568                >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
569    }
570
571    private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
572        // Get the number of accepted suggestions
573        ContentResolver cr = getContext().getContentResolver();
574        final int numSuggestions = Settings.Secure.getInt(cr,
575                Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
576
577        // Increment the number of accepted suggestions only if it would change intro mode
578        if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
579            Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
580                    numSuggestions + 1);
581        }
582    }
583
584    // Injected from StatusBar at creation.
585    public void setCurrentSysuiVisibility(int systemUiVisibility) {
586        mSystemUiVisibility = systemUiVisibility;
587        mNavigationBarMode = mStatusBar.computeBarMode(0, mSystemUiVisibility,
588                View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
589                View.NAVIGATION_BAR_TRANSPARENT);
590        checkNavBarModes();
591        mStatusBar.touchAutoHide();
592        mLightBarController.onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */,
593                true /* nbModeChanged */, mNavigationBarMode);
594    }
595
596    @Override
597    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
598            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
599        final int oldVal = mSystemUiVisibility;
600        final int newVal = (oldVal & ~mask) | (vis & mask);
601        final int diff = newVal ^ oldVal;
602        boolean nbModeChanged = false;
603        if (diff != 0) {
604            mSystemUiVisibility = newVal;
605
606            // update navigation bar mode
607            final int nbMode = getView() == null
608                    ? -1 : mStatusBar.computeBarMode(oldVal, newVal,
609                    View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
610                    View.NAVIGATION_BAR_TRANSPARENT);
611            nbModeChanged = nbMode != -1;
612            if (nbModeChanged) {
613                if (mNavigationBarMode != nbMode) {
614                    mNavigationBarMode = nbMode;
615                    checkNavBarModes();
616                }
617                mStatusBar.touchAutoHide();
618            }
619        }
620
621        mLightBarController.onNavigationVisibilityChanged(vis, mask, nbModeChanged,
622                mNavigationBarMode);
623    }
624
625    @Override
626    public void disable(int state1, int state2, boolean animate) {
627        // All navigation bar flags are in state1.
628        int masked = state1 & (StatusBarManager.DISABLE_HOME
629                | StatusBarManager.DISABLE_RECENT
630                | StatusBarManager.DISABLE_BACK
631                | StatusBarManager.DISABLE_SEARCH);
632        if (masked != mDisabledFlags1) {
633            mDisabledFlags1 = masked;
634            if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state1);
635            updateScreenPinningGestures();
636        }
637    }
638
639    // ----- Internal stuffz -----
640
641    private void refreshLayout(int layoutDirection) {
642        if (mNavigationBarView != null) {
643            mNavigationBarView.setLayoutDirection(layoutDirection);
644        }
645    }
646
647    private boolean shouldDisableNavbarGestures() {
648        return !mStatusBar.isDeviceProvisioned()
649                || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
650    }
651
652    private void repositionNavigationBar() {
653        if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return;
654
655        prepareNavigationBarView();
656
657        mWindowManager.updateViewLayout((View) mNavigationBarView.getParent(),
658                ((View) mNavigationBarView.getParent()).getLayoutParams());
659    }
660
661    private void updateScreenPinningGestures() {
662        if (mNavigationBarView == null) {
663            return;
664        }
665
666        // Change the cancel pin gesture to home and back if recents button is invisible
667        boolean recentsVisible = mNavigationBarView.isRecentsButtonVisible();
668        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
669        if (recentsVisible) {
670            backButton.setOnLongClickListener(this::onLongPressBackRecents);
671        } else {
672            backButton.setOnLongClickListener(this::onLongPressBackHome);
673        }
674    }
675
676    private void notifyNavigationBarScreenOn() {
677        mNavigationBarView.updateNavButtonIcons();
678    }
679
680    private void prepareNavigationBarView() {
681        mNavigationBarView.reorient();
682
683        ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
684        recentsButton.setOnClickListener(this::onRecentsClick);
685        recentsButton.setOnTouchListener(this::onRecentsTouch);
686        recentsButton.setLongClickable(true);
687        recentsButton.setOnLongClickListener(this::onLongPressBackRecents);
688
689        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
690        backButton.setLongClickable(true);
691
692        ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
693        homeButton.setOnTouchListener(this::onHomeTouch);
694        homeButton.setOnLongClickListener(this::onHomeLongClick);
695
696        ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton();
697        accessibilityButton.setOnClickListener(this::onAccessibilityClick);
698        accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
699        updateAccessibilityServicesState(mAccessibilityManager);
700
701        ButtonDispatcher rotateSuggestionButton = mNavigationBarView.getRotateSuggestionButton();
702        rotateSuggestionButton.setOnClickListener(this::onRotateSuggestionClick);
703        rotateSuggestionButton.setOnHoverListener(this::onRotateSuggestionHover);
704        updateScreenPinningGestures();
705    }
706
707    private boolean onHomeTouch(View v, MotionEvent event) {
708        if (mHomeBlockedThisTouch && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
709            return true;
710        }
711        // If an incoming call is ringing, HOME is totally disabled.
712        // (The user is already on the InCallUI at this point,
713        // and his ONLY options are to answer or reject the call.)
714        switch (event.getAction()) {
715            case MotionEvent.ACTION_DOWN:
716                mHomeBlockedThisTouch = false;
717                TelecomManager telecomManager =
718                        getContext().getSystemService(TelecomManager.class);
719                if (telecomManager != null && telecomManager.isRinging()) {
720                    if (mStatusBar.isKeyguardShowing()) {
721                        Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
722                                "No heads up");
723                        mHomeBlockedThisTouch = true;
724                        return true;
725                    }
726                }
727                break;
728            case MotionEvent.ACTION_UP:
729            case MotionEvent.ACTION_CANCEL:
730                mStatusBar.awakenDreams();
731                break;
732        }
733        return false;
734    }
735
736    private void onVerticalChanged(boolean isVertical) {
737        mStatusBar.setQsScrimEnabled(!isVertical);
738    }
739
740    private boolean onNavigationTouch(View v, MotionEvent event) {
741        mStatusBar.checkUserAutohide(event);
742        return false;
743    }
744
745    @VisibleForTesting
746    boolean onHomeLongClick(View v) {
747        if (!mNavigationBarView.isRecentsButtonVisible()
748                && ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
749            return onLongPressBackHome(v);
750        }
751        if (shouldDisableNavbarGestures()) {
752            return false;
753        }
754        mNavigationBarView.onNavigationButtonLongPress(v);
755        mMetricsLogger.action(MetricsEvent.ACTION_ASSIST_LONG_PRESS);
756        mAssistManager.startAssist(new Bundle() /* args */);
757        mStatusBar.awakenDreams();
758
759        if (mNavigationBarView != null) {
760            mNavigationBarView.abortCurrentGesture();
761        }
762        return true;
763    }
764
765    // additional optimization when we have software system buttons - start loading the recent
766    // tasks on touch down
767    private boolean onRecentsTouch(View v, MotionEvent event) {
768        int action = event.getAction() & MotionEvent.ACTION_MASK;
769        if (action == MotionEvent.ACTION_DOWN) {
770            mCommandQueue.preloadRecentApps();
771        } else if (action == MotionEvent.ACTION_CANCEL) {
772            mCommandQueue.cancelPreloadRecentApps();
773        } else if (action == MotionEvent.ACTION_UP) {
774            if (!v.isPressed()) {
775                mCommandQueue.cancelPreloadRecentApps();
776            }
777        }
778        return false;
779    }
780
781    private void onRecentsClick(View v) {
782        if (LatencyTracker.isEnabled(getContext())) {
783            LatencyTracker.getInstance(getContext()).onActionStart(
784                    LatencyTracker.ACTION_TOGGLE_RECENTS);
785        }
786        mStatusBar.awakenDreams();
787        mCommandQueue.toggleRecentApps();
788    }
789
790    private boolean onLongPressBackHome(View v) {
791        mNavigationBarView.onNavigationButtonLongPress(v);
792        return onLongPressNavigationButtons(v, R.id.back, R.id.home);
793    }
794
795    private boolean onLongPressBackRecents(View v) {
796        mNavigationBarView.onNavigationButtonLongPress(v);
797        return onLongPressNavigationButtons(v, R.id.back, R.id.recent_apps);
798    }
799
800    /**
801     * This handles long-press of both back and recents/home. Back is the common button with
802     * combination of recents if it is visible or home if recents is invisible.
803     * They are handled together to capture them both being long-pressed
804     * at the same time to exit screen pinning (lock task).
805     *
806     * When accessibility mode is on, only a long-press from recents/home
807     * is required to exit.
808     *
809     * In all other circumstances we try to pass through long-press events
810     * for Back, so that apps can still use it.  Which can be from two things.
811     * 1) Not currently in screen pinning (lock task).
812     * 2) Back is long-pressed without recents/home.
813     */
814    private boolean onLongPressNavigationButtons(View v, @IdRes int btnId1, @IdRes int btnId2) {
815        try {
816            boolean sendBackLongPress = false;
817            IActivityManager activityManager = ActivityManagerNative.getDefault();
818            boolean touchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
819            boolean inLockTaskMode = activityManager.isInLockTaskMode();
820            if (inLockTaskMode && !touchExplorationEnabled) {
821                long time = System.currentTimeMillis();
822
823                // If we recently long-pressed the other button then they were
824                // long-pressed 'together'
825                if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) {
826                    activityManager.stopSystemLockTaskMode();
827                    // When exiting refresh disabled flags.
828                    mNavigationBarView.updateNavButtonIcons();
829                    return true;
830                } else if (v.getId() == btnId1) {
831                    ButtonDispatcher button = btnId2 == R.id.recent_apps
832                            ? mNavigationBarView.getRecentsButton()
833                            : mNavigationBarView.getHomeButton();
834                    if (!button.getCurrentView().isPressed()) {
835                        // If we aren't pressing recents/home right now then they presses
836                        // won't be together, so send the standard long-press action.
837                        sendBackLongPress = true;
838                    }
839                }
840                mLastLockToAppLongPress = time;
841            } else {
842                // If this is back still need to handle sending the long-press event.
843                if (v.getId() == btnId1) {
844                    sendBackLongPress = true;
845                } else if (touchExplorationEnabled && inLockTaskMode) {
846                    // When in accessibility mode a long press that is recents/home (not back)
847                    // should stop lock task.
848                    activityManager.stopSystemLockTaskMode();
849                    // When exiting refresh disabled flags.
850                    mNavigationBarView.updateNavButtonIcons();
851                    return true;
852                } else if (v.getId() == btnId2) {
853                    return btnId2 == R.id.recent_apps
854                            ? onLongPressRecents()
855                            : onHomeLongClick(mNavigationBarView.getHomeButton().getCurrentView());
856                }
857            }
858            if (sendBackLongPress) {
859                KeyButtonView keyButtonView = (KeyButtonView) v;
860                keyButtonView.sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
861                keyButtonView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
862                return true;
863            }
864        } catch (RemoteException e) {
865            Log.d(TAG, "Unable to reach activity manager", e);
866        }
867        return false;
868    }
869
870    private boolean onLongPressRecents() {
871        if (mRecents == null || !ActivityManager.supportsMultiWindow(getContext())
872                || !mDivider.getView().getSnapAlgorithm().isSplitScreenFeasible()
873                || Recents.getConfiguration().isLowRamDevice) {
874            return false;
875        }
876
877        return mStatusBar.toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
878                MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
879    }
880
881    private void onAccessibilityClick(View v) {
882        mAccessibilityManager.notifyAccessibilityButtonClicked();
883    }
884
885    private boolean onAccessibilityLongClick(View v) {
886        Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
887        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
888        v.getContext().startActivityAsUser(intent, UserHandle.CURRENT);
889        return true;
890    }
891
892    private void updateAccessibilityServicesState(AccessibilityManager accessibilityManager) {
893        int requestingServices = 0;
894        try {
895            if (Settings.Secure.getIntForUser(mContentResolver,
896                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
897                    UserHandle.USER_CURRENT) == 1) {
898                requestingServices++;
899            }
900        } catch (Settings.SettingNotFoundException e) {
901        }
902
903        boolean feedbackEnabled = false;
904        // AccessibilityManagerService resolves services for the current user since the local
905        // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
906        final List<AccessibilityServiceInfo> services =
907                accessibilityManager.getEnabledAccessibilityServiceList(
908                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
909        for (int i = services.size() - 1; i >= 0; --i) {
910            AccessibilityServiceInfo info = services.get(i);
911            if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
912                requestingServices++;
913            }
914
915            if (info.feedbackType != 0 && info.feedbackType !=
916                    AccessibilityServiceInfo.FEEDBACK_GENERIC) {
917                feedbackEnabled = true;
918            }
919        }
920
921        mAccessibilityFeedbackEnabled = feedbackEnabled;
922
923        final boolean showAccessibilityButton = requestingServices >= 1;
924        final boolean targetSelection = requestingServices >= 2;
925        mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection);
926    }
927
928    private void onRotateSuggestionClick(View v) {
929        mMetricsLogger.action(MetricsEvent.ACTION_ROTATION_SUGGESTION_ACCEPTED);
930        incrementNumAcceptedRotationSuggestionsIfNeeded();
931        mRotationLockController.setRotationLockedAtAngle(true, mLastRotationSuggestion);
932    }
933
934    private boolean onRotateSuggestionHover(View v, MotionEvent event) {
935        final int action = event.getActionMasked();
936        mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
937                || (action == MotionEvent.ACTION_HOVER_MOVE);
938        rescheduleRotationTimeout(true);
939        return false; // Must return false so a11y hover events are dispatched correctly.
940    }
941
942    // ----- Methods that StatusBar talks to (should be minimized) -----
943
944    public void setLightBarController(LightBarController lightBarController) {
945        mLightBarController = lightBarController;
946        mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController());
947    }
948
949    public boolean isSemiTransparent() {
950        return mNavigationBarMode == MODE_SEMI_TRANSPARENT;
951    }
952
953    public void disableAnimationsDuringHide(long delay) {
954        mNavigationBarView.setLayoutTransitionsEnabled(false);
955        mNavigationBarView.postDelayed(() -> mNavigationBarView.setLayoutTransitionsEnabled(true),
956                delay + StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
957    }
958
959    public BarTransitions getBarTransitions() {
960        return mNavigationBarView.getBarTransitions();
961    }
962
963    public void checkNavBarModes() {
964        mStatusBar.checkBarMode(mNavigationBarMode,
965                mNavigationBarWindowState, mNavigationBarView.getBarTransitions());
966    }
967
968    public void finishBarAnimations() {
969        mNavigationBarView.getBarTransitions().finishAnimations();
970    }
971
972    private final AccessibilityServicesStateChangeListener mAccessibilityListener =
973            this::updateAccessibilityServicesState;
974
975    private class MagnificationContentObserver extends ContentObserver {
976
977        public MagnificationContentObserver(Handler handler) {
978            super(handler);
979        }
980
981        @Override
982        public void onChange(boolean selfChange) {
983            NavigationBarFragment.this.updateAccessibilityServicesState(mAccessibilityManager);
984        }
985    }
986
987    private final Stub mRotationWatcher = new Stub() {
988        @Override
989        public void onRotationChanged(final int rotation) throws RemoteException {
990            // We need this to be scheduled as early as possible to beat the redrawing of
991            // window in response to the orientation change.
992            Handler h = getView().getHandler();
993            Message msg = Message.obtain(h, () -> {
994
995                // If the screen rotation changes while locked, potentially update lock to flow with
996                // new screen rotation and hide any showing suggestions.
997                if (mRotationLockController.isRotationLocked()) {
998                    if (shouldOverrideUserLockPrefs(rotation)) {
999                        mRotationLockController.setRotationLockedAtAngle(true, rotation);
1000                    }
1001                    setRotateSuggestionButtonState(false, true);
1002                }
1003
1004                if (mNavigationBarView != null
1005                        && mNavigationBarView.needsReorient(rotation)) {
1006                    repositionNavigationBar();
1007                }
1008            });
1009            msg.setAsynchronous(true);
1010            h.sendMessageAtFrontOfQueue(msg);
1011        }
1012
1013        private boolean shouldOverrideUserLockPrefs(final int rotation) {
1014            // Only override user prefs when returning to the natural rotation (normally portrait).
1015            // Don't let apps that force landscape or 180 alter user lock.
1016            return rotation == NATURAL_ROTATION;
1017        }
1018    };
1019
1020    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
1021        @Override
1022        public void onReceive(Context context, Intent intent) {
1023            String action = intent.getAction();
1024            if (Intent.ACTION_SCREEN_OFF.equals(action)
1025                    || Intent.ACTION_SCREEN_ON.equals(action)) {
1026                notifyNavigationBarScreenOn();
1027            }
1028        }
1029    };
1030
1031    class TaskStackListenerImpl extends SysUiTaskStackChangeListener {
1032        // Invalidate any rotation suggestion on task change or activity orientation change
1033        // Note: all callbacks happen on main thread
1034
1035        @Override
1036        public void onTaskStackChanged() {
1037            setRotateSuggestionButtonState(false);
1038        }
1039
1040        @Override
1041        public void onTaskRemoved(int taskId) {
1042            setRotateSuggestionButtonState(false);
1043        }
1044
1045        @Override
1046        public void onTaskMovedToFront(int taskId) {
1047            setRotateSuggestionButtonState(false);
1048        }
1049
1050        @Override
1051        public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
1052            // Only hide the icon if the top task changes its requestedOrientation
1053            // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
1054            final boolean top = ActivityManagerWrapper.getInstance().getRunningTask().id == taskId;
1055            if (top) setRotateSuggestionButtonState(false);
1056        }
1057    }
1058
1059    private class ViewRippler {
1060        private static final int RIPPLE_OFFSET_MS = 50;
1061        private static final int RIPPLE_INTERVAL_MS = 2000;
1062        private View mRoot;
1063
1064        public void start(View root) {
1065            stop(); // Stop any pending ripple animations
1066
1067            mRoot = root;
1068
1069            // Schedule pending ripples, offset the 1st to avoid problems with visibility change
1070            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
1071            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
1072            mRoot.postOnAnimationDelayed(mRipple, 2*RIPPLE_INTERVAL_MS);
1073        }
1074
1075        public void stop() {
1076            if (mRoot != null) mRoot.removeCallbacks(mRipple);
1077        }
1078
1079        private final Runnable mRipple = new Runnable() {
1080            @Override
1081            public void run() { // Cause the ripple to fire via false presses
1082                mRoot.setPressed(true);
1083                mRoot.setPressed(false);
1084            }
1085        };
1086    }
1087
1088    public static View create(Context context, FragmentListener listener) {
1089        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
1090                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
1091                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
1092                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
1093                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
1094                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
1095                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
1096                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
1097                        | WindowManager.LayoutParams.FLAG_SLIPPERY,
1098                PixelFormat.TRANSLUCENT);
1099        lp.token = new Binder();
1100        lp.setTitle("NavigationBar");
1101        lp.accessibilityTitle = context.getString(R.string.nav_bar);
1102        lp.windowAnimations = 0;
1103
1104        View navigationBarView = LayoutInflater.from(context).inflate(
1105                R.layout.navigation_bar_window, null);
1106
1107        if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + navigationBarView);
1108        if (navigationBarView == null) return null;
1109
1110        context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
1111        FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
1112        NavigationBarFragment fragment = new NavigationBarFragment();
1113        fragmentHost.getFragmentManager().beginTransaction()
1114                .replace(R.id.navigation_bar_frame, fragment, TAG)
1115                .commit();
1116        fragmentHost.addTagListener(TAG, listener);
1117        return navigationBarView;
1118    }
1119}
1120