NavigationBarFragment.java revision feff6c7d41fe3c78bef976afadc3f9bfcac7fb0e
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        switch (backDisposition) {
347            case InputMethodService.BACK_DISPOSITION_DEFAULT:
348            case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
349            case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
350                if (imeShown) {
351                    hints |= NAVIGATION_HINT_BACK_ALT;
352                } else {
353                    hints &= ~NAVIGATION_HINT_BACK_ALT;
354                }
355                break;
356            case InputMethodService.BACK_DISPOSITION_ADJUST_NOTHING:
357                hints &= ~NAVIGATION_HINT_BACK_ALT;
358                break;
359        }
360        if (showImeSwitcher) {
361            hints |= NAVIGATION_HINT_IME_SHOWN;
362        } else {
363            hints &= ~NAVIGATION_HINT_IME_SHOWN;
364        }
365        if (hints == mNavigationIconHints) return;
366
367        mNavigationIconHints = hints;
368
369        if (mNavigationBarView != null) {
370            mNavigationBarView.setNavigationIconHints(hints);
371        }
372        mStatusBar.checkBarModes();
373    }
374
375    @Override
376    public void topAppWindowChanged(boolean showMenu) {
377        if (mNavigationBarView != null) {
378            mNavigationBarView.setMenuVisibility(showMenu);
379        }
380    }
381
382    @Override
383    public void setWindowState(int window, int state) {
384        if (mNavigationBarView != null
385                && window == StatusBarManager.WINDOW_NAVIGATION_BAR
386                && mNavigationBarWindowState != state) {
387            mNavigationBarWindowState = state;
388            if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
389
390            // If the navbar is visible, show the rotate button if there's a pending suggestion
391            if (state == WINDOW_STATE_SHOWING && mPendingRotationSuggestion) {
392                showAndLogRotationSuggestion();
393            }
394        }
395    }
396
397    @Override
398    public void onRotationProposal(final int rotation, boolean isValid) {
399        // This method will be called on rotation suggestion changes even if the proposed rotation
400        // is not valid for the top app. Use invalid rotation choices as a signal to remove the
401        // rotate button if shown.
402        if (!isValid) {
403            setRotateSuggestionButtonState(false);
404            return;
405        }
406
407        // If window rotation matches suggested rotation, remove any current suggestions
408        final int winRotation = mWindowManager.getDefaultDisplay().getRotation();
409        if (rotation == winRotation) {
410            getView().removeCallbacks(mRemoveRotationProposal);
411            setRotateSuggestionButtonState(false);
412            return;
413        }
414
415        // Prepare to show the navbar icon by updating the icon style to change anim params
416        mLastRotationSuggestion = rotation; // Remember rotation for click
417        if (mNavigationBarView != null) {
418            final boolean rotationCCW = isRotationAnimationCCW(winRotation, rotation);
419            int style;
420            if (winRotation == Surface.ROTATION_0 || winRotation == Surface.ROTATION_180) {
421                style = rotationCCW ? R.style.RotateButtonCCWStart90 :
422                        R.style.RotateButtonCWStart90;
423            } else { // 90 or 270
424                style = rotationCCW ? R.style.RotateButtonCCWStart0 :
425                        R.style.RotateButtonCWStart0;
426            }
427            mNavigationBarView.updateRotateSuggestionButtonStyle(style, true);
428        }
429
430        if (mNavigationBarWindowState != WINDOW_STATE_SHOWING) {
431            // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
432            // visible given some time limit.
433            mPendingRotationSuggestion = true;
434            getView().removeCallbacks(mCancelPendingRotationProposal);
435            getView().postDelayed(mCancelPendingRotationProposal,
436                    NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
437
438        } else { // The navbar is visible so show the icon right away
439            showAndLogRotationSuggestion();
440        }
441    }
442
443    private void showAndLogRotationSuggestion() {
444        setRotateSuggestionButtonState(true);
445        rescheduleRotationTimeout(false);
446        mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
447    }
448
449    private boolean isRotationAnimationCCW(int from, int to) {
450        // All 180deg WM rotation animations are CCW, match that
451        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
452        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
453        if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
454        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
455        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
456        if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
457        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
458        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
459        if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
460        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
461        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
462        if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
463        return false; // Default
464    }
465
466    public void setRotateSuggestionButtonState(final boolean visible) {
467        setRotateSuggestionButtonState(visible, false);
468    }
469
470    public void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
471        if (mNavigationBarView == null) return;
472
473        // At any point the the button can become invisible because an a11y service became active.
474        // Similarly, a call to make the button visible may be rejected because an a11y service is
475        // active. Must account for this.
476
477        ButtonDispatcher rotBtn = mNavigationBarView.getRotateSuggestionButton();
478        final boolean currentlyVisible = mNavigationBarView.isRotateButtonVisible();
479
480        // Rerun a show animation to indicate change but don't rerun a hide animation
481        if (!visible && !currentlyVisible) return;
482
483        View view = rotBtn.getCurrentView();
484        if (view == null) return;
485
486        KeyButtonDrawable kbd = rotBtn.getImageDrawable();
487        if (kbd == null) return;
488
489        // The KBD and AVD is recreated every new valid suggestion because of style changes.
490        AnimatedVectorDrawable animIcon = null;
491        if (kbd.getDrawable(0) instanceof AnimatedVectorDrawable) {
492            animIcon = (AnimatedVectorDrawable) kbd.getDrawable(0);
493        }
494
495        // Clear any pending suggestion flag as it has either been nullified or is being shown
496        mPendingRotationSuggestion = false;
497        getView().removeCallbacks(mCancelPendingRotationProposal);
498
499        // Handle the visibility change and animation
500        if (visible) { // Appear and change (cannot force)
501            // Stop and clear any currently running hide animations
502            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
503                mRotateHideAnimator.cancel();
504            }
505            mRotateHideAnimator = null;
506
507            // Reset the alpha if any has changed due to hide animation
508            view.setAlpha(1f);
509
510            // Run the rotate icon's animation if it has one
511            if (animIcon != null) {
512                animIcon.reset();
513                animIcon.start();
514            }
515
516            if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
517
518            // Set visibility, may fail if a11y service is active.
519            // If invisible, call will stop animation.
520            mNavigationBarView.setRotateButtonVisibility(true);
521
522        } else { // Hide
523
524            mViewRippler.stop(); // Prevent any pending ripples, force hide or not
525
526            if (force) {
527                // If a hide animator is running stop it and make invisible
528                if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
529                    mRotateHideAnimator.pause();
530                }
531                mNavigationBarView.setRotateButtonVisibility(false);
532                return;
533            }
534
535            // Don't start any new hide animations if one is running
536            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
537
538            ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha",
539                    0f);
540            fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
541            fadeOut.setInterpolator(Interpolators.LINEAR);
542            fadeOut.addListener(new AnimatorListenerAdapter() {
543                @Override
544                public void onAnimationEnd(Animator animation) {
545                    mNavigationBarView.setRotateButtonVisibility(false);
546                }
547            });
548
549            mRotateHideAnimator = fadeOut;
550            fadeOut.start();
551        }
552    }
553
554    private void rescheduleRotationTimeout(final boolean reasonHover) {
555        // May be called due to a new rotation proposal or a change in hover state
556        if (reasonHover) {
557            // Don't reschedule if a hide animator is running
558            if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
559            // Don't reschedule if not visible
560            if (!mNavigationBarView.isRotateButtonVisible()) return;
561        }
562
563        getView().removeCallbacks(mRemoveRotationProposal); // Stop any pending removal
564        getView().postDelayed(mRemoveRotationProposal,
565                computeRotationProposalTimeout()); // Schedule timeout
566    }
567
568    private int computeRotationProposalTimeout() {
569        if (mAccessibilityFeedbackEnabled) return 20000;
570        if (mHoveringRotationSuggestion) return 16000;
571        return 6000;
572    }
573
574    private boolean isRotateSuggestionIntroduced() {
575        ContentResolver cr = getContext().getContentResolver();
576        return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
577                >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
578    }
579
580    private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
581        // Get the number of accepted suggestions
582        ContentResolver cr = getContext().getContentResolver();
583        final int numSuggestions = Settings.Secure.getInt(cr,
584                Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
585
586        // Increment the number of accepted suggestions only if it would change intro mode
587        if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
588            Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
589                    numSuggestions + 1);
590        }
591    }
592
593    // Injected from StatusBar at creation.
594    public void setCurrentSysuiVisibility(int systemUiVisibility) {
595        mSystemUiVisibility = systemUiVisibility;
596        mNavigationBarMode = mStatusBar.computeBarMode(0, mSystemUiVisibility,
597                View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
598                View.NAVIGATION_BAR_TRANSPARENT);
599        checkNavBarModes();
600        mStatusBar.touchAutoHide();
601        mLightBarController.onNavigationVisibilityChanged(mSystemUiVisibility, 0 /* mask */,
602                true /* nbModeChanged */, mNavigationBarMode);
603    }
604
605    @Override
606    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
607            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
608        final int oldVal = mSystemUiVisibility;
609        final int newVal = (oldVal & ~mask) | (vis & mask);
610        final int diff = newVal ^ oldVal;
611        boolean nbModeChanged = false;
612        if (diff != 0) {
613            mSystemUiVisibility = newVal;
614
615            // update navigation bar mode
616            final int nbMode = getView() == null
617                    ? -1 : mStatusBar.computeBarMode(oldVal, newVal,
618                    View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
619                    View.NAVIGATION_BAR_TRANSPARENT);
620            nbModeChanged = nbMode != -1;
621            if (nbModeChanged) {
622                if (mNavigationBarMode != nbMode) {
623                    mNavigationBarMode = nbMode;
624                    checkNavBarModes();
625                }
626                mStatusBar.touchAutoHide();
627            }
628        }
629
630        mLightBarController.onNavigationVisibilityChanged(vis, mask, nbModeChanged,
631                mNavigationBarMode);
632    }
633
634    @Override
635    public void disable(int state1, int state2, boolean animate) {
636        // All navigation bar flags are in state1.
637        int masked = state1 & (StatusBarManager.DISABLE_HOME
638                | StatusBarManager.DISABLE_RECENT
639                | StatusBarManager.DISABLE_BACK
640                | StatusBarManager.DISABLE_SEARCH);
641        if (masked != mDisabledFlags1) {
642            mDisabledFlags1 = masked;
643            if (mNavigationBarView != null) mNavigationBarView.setDisabledFlags(state1);
644            updateScreenPinningGestures();
645        }
646    }
647
648    // ----- Internal stuffz -----
649
650    private void refreshLayout(int layoutDirection) {
651        if (mNavigationBarView != null) {
652            mNavigationBarView.setLayoutDirection(layoutDirection);
653        }
654    }
655
656    private boolean shouldDisableNavbarGestures() {
657        return !mStatusBar.isDeviceProvisioned()
658                || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
659    }
660
661    private void repositionNavigationBar() {
662        if (mNavigationBarView == null || !mNavigationBarView.isAttachedToWindow()) return;
663
664        prepareNavigationBarView();
665
666        mWindowManager.updateViewLayout((View) mNavigationBarView.getParent(),
667                ((View) mNavigationBarView.getParent()).getLayoutParams());
668    }
669
670    private void updateScreenPinningGestures() {
671        if (mNavigationBarView == null) {
672            return;
673        }
674
675        // Change the cancel pin gesture to home and back if recents button is invisible
676        boolean recentsVisible = mNavigationBarView.isRecentsButtonVisible();
677        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
678        if (recentsVisible) {
679            backButton.setOnLongClickListener(this::onLongPressBackRecents);
680        } else {
681            backButton.setOnLongClickListener(this::onLongPressBackHome);
682        }
683    }
684
685    private void notifyNavigationBarScreenOn() {
686        mNavigationBarView.updateNavButtonIcons();
687    }
688
689    private void prepareNavigationBarView() {
690        mNavigationBarView.reorient();
691
692        ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
693        recentsButton.setOnClickListener(this::onRecentsClick);
694        recentsButton.setOnTouchListener(this::onRecentsTouch);
695        recentsButton.setLongClickable(true);
696        recentsButton.setOnLongClickListener(this::onLongPressBackRecents);
697
698        ButtonDispatcher backButton = mNavigationBarView.getBackButton();
699        backButton.setLongClickable(true);
700
701        ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
702        homeButton.setOnTouchListener(this::onHomeTouch);
703        homeButton.setOnLongClickListener(this::onHomeLongClick);
704
705        ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton();
706        accessibilityButton.setOnClickListener(this::onAccessibilityClick);
707        accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
708        updateAccessibilityServicesState(mAccessibilityManager);
709
710        ButtonDispatcher rotateSuggestionButton = mNavigationBarView.getRotateSuggestionButton();
711        rotateSuggestionButton.setOnClickListener(this::onRotateSuggestionClick);
712        rotateSuggestionButton.setOnHoverListener(this::onRotateSuggestionHover);
713        updateScreenPinningGestures();
714    }
715
716    private boolean onHomeTouch(View v, MotionEvent event) {
717        if (mHomeBlockedThisTouch && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
718            return true;
719        }
720        // If an incoming call is ringing, HOME is totally disabled.
721        // (The user is already on the InCallUI at this point,
722        // and his ONLY options are to answer or reject the call.)
723        switch (event.getAction()) {
724            case MotionEvent.ACTION_DOWN:
725                mHomeBlockedThisTouch = false;
726                TelecomManager telecomManager =
727                        getContext().getSystemService(TelecomManager.class);
728                if (telecomManager != null && telecomManager.isRinging()) {
729                    if (mStatusBar.isKeyguardShowing()) {
730                        Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
731                                "No heads up");
732                        mHomeBlockedThisTouch = true;
733                        return true;
734                    }
735                }
736                break;
737            case MotionEvent.ACTION_UP:
738            case MotionEvent.ACTION_CANCEL:
739                mStatusBar.awakenDreams();
740                break;
741        }
742        return false;
743    }
744
745    private void onVerticalChanged(boolean isVertical) {
746        mStatusBar.setQsScrimEnabled(!isVertical);
747    }
748
749    private boolean onNavigationTouch(View v, MotionEvent event) {
750        mStatusBar.checkUserAutohide(event);
751        return false;
752    }
753
754    @VisibleForTesting
755    boolean onHomeLongClick(View v) {
756        if (!mNavigationBarView.isRecentsButtonVisible()
757                && ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
758            return onLongPressBackHome(v);
759        }
760        if (shouldDisableNavbarGestures()) {
761            return false;
762        }
763        mNavigationBarView.onNavigationButtonLongPress(v);
764        mMetricsLogger.action(MetricsEvent.ACTION_ASSIST_LONG_PRESS);
765        mAssistManager.startAssist(new Bundle() /* args */);
766        mStatusBar.awakenDreams();
767
768        if (mNavigationBarView != null) {
769            mNavigationBarView.abortCurrentGesture();
770        }
771        return true;
772    }
773
774    // additional optimization when we have software system buttons - start loading the recent
775    // tasks on touch down
776    private boolean onRecentsTouch(View v, MotionEvent event) {
777        int action = event.getAction() & MotionEvent.ACTION_MASK;
778        if (action == MotionEvent.ACTION_DOWN) {
779            mCommandQueue.preloadRecentApps();
780        } else if (action == MotionEvent.ACTION_CANCEL) {
781            mCommandQueue.cancelPreloadRecentApps();
782        } else if (action == MotionEvent.ACTION_UP) {
783            if (!v.isPressed()) {
784                mCommandQueue.cancelPreloadRecentApps();
785            }
786        }
787        return false;
788    }
789
790    private void onRecentsClick(View v) {
791        if (LatencyTracker.isEnabled(getContext())) {
792            LatencyTracker.getInstance(getContext()).onActionStart(
793                    LatencyTracker.ACTION_TOGGLE_RECENTS);
794        }
795        mStatusBar.awakenDreams();
796        mCommandQueue.toggleRecentApps();
797    }
798
799    private boolean onLongPressBackHome(View v) {
800        mNavigationBarView.onNavigationButtonLongPress(v);
801        return onLongPressNavigationButtons(v, R.id.back, R.id.home);
802    }
803
804    private boolean onLongPressBackRecents(View v) {
805        mNavigationBarView.onNavigationButtonLongPress(v);
806        return onLongPressNavigationButtons(v, R.id.back, R.id.recent_apps);
807    }
808
809    /**
810     * This handles long-press of both back and recents/home. Back is the common button with
811     * combination of recents if it is visible or home if recents is invisible.
812     * They are handled together to capture them both being long-pressed
813     * at the same time to exit screen pinning (lock task).
814     *
815     * When accessibility mode is on, only a long-press from recents/home
816     * is required to exit.
817     *
818     * In all other circumstances we try to pass through long-press events
819     * for Back, so that apps can still use it.  Which can be from two things.
820     * 1) Not currently in screen pinning (lock task).
821     * 2) Back is long-pressed without recents/home.
822     */
823    private boolean onLongPressNavigationButtons(View v, @IdRes int btnId1, @IdRes int btnId2) {
824        try {
825            boolean sendBackLongPress = false;
826            IActivityManager activityManager = ActivityManagerNative.getDefault();
827            boolean touchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
828            boolean inLockTaskMode = activityManager.isInLockTaskMode();
829            if (inLockTaskMode && !touchExplorationEnabled) {
830                long time = System.currentTimeMillis();
831
832                // If we recently long-pressed the other button then they were
833                // long-pressed 'together'
834                if ((time - mLastLockToAppLongPress) < LOCK_TO_APP_GESTURE_TOLERENCE) {
835                    activityManager.stopSystemLockTaskMode();
836                    // When exiting refresh disabled flags.
837                    mNavigationBarView.updateNavButtonIcons();
838                    return true;
839                } else if (v.getId() == btnId1) {
840                    ButtonDispatcher button = btnId2 == R.id.recent_apps
841                            ? mNavigationBarView.getRecentsButton()
842                            : mNavigationBarView.getHomeButton();
843                    if (!button.getCurrentView().isPressed()) {
844                        // If we aren't pressing recents/home right now then they presses
845                        // won't be together, so send the standard long-press action.
846                        sendBackLongPress = true;
847                    }
848                }
849                mLastLockToAppLongPress = time;
850            } else {
851                // If this is back still need to handle sending the long-press event.
852                if (v.getId() == btnId1) {
853                    sendBackLongPress = true;
854                } else if (touchExplorationEnabled && inLockTaskMode) {
855                    // When in accessibility mode a long press that is recents/home (not back)
856                    // should stop lock task.
857                    activityManager.stopSystemLockTaskMode();
858                    // When exiting refresh disabled flags.
859                    mNavigationBarView.updateNavButtonIcons();
860                    return true;
861                } else if (v.getId() == btnId2) {
862                    return btnId2 == R.id.recent_apps
863                            ? onLongPressRecents()
864                            : onHomeLongClick(mNavigationBarView.getHomeButton().getCurrentView());
865                }
866            }
867            if (sendBackLongPress) {
868                KeyButtonView keyButtonView = (KeyButtonView) v;
869                keyButtonView.sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS);
870                keyButtonView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
871                return true;
872            }
873        } catch (RemoteException e) {
874            Log.d(TAG, "Unable to reach activity manager", e);
875        }
876        return false;
877    }
878
879    private boolean onLongPressRecents() {
880        if (mRecents == null || !ActivityManager.supportsMultiWindow(getContext())
881                || !mDivider.getView().getSnapAlgorithm().isSplitScreenFeasible()
882                || Recents.getConfiguration().isLowRamDevice) {
883            return false;
884        }
885
886        return mStatusBar.toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
887                MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
888    }
889
890    private void onAccessibilityClick(View v) {
891        mAccessibilityManager.notifyAccessibilityButtonClicked();
892    }
893
894    private boolean onAccessibilityLongClick(View v) {
895        Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
896        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
897        v.getContext().startActivityAsUser(intent, UserHandle.CURRENT);
898        return true;
899    }
900
901    private void updateAccessibilityServicesState(AccessibilityManager accessibilityManager) {
902        int requestingServices = 0;
903        try {
904            if (Settings.Secure.getIntForUser(mContentResolver,
905                    Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
906                    UserHandle.USER_CURRENT) == 1) {
907                requestingServices++;
908            }
909        } catch (Settings.SettingNotFoundException e) {
910        }
911
912        boolean feedbackEnabled = false;
913        // AccessibilityManagerService resolves services for the current user since the local
914        // AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
915        final List<AccessibilityServiceInfo> services =
916                accessibilityManager.getEnabledAccessibilityServiceList(
917                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
918        for (int i = services.size() - 1; i >= 0; --i) {
919            AccessibilityServiceInfo info = services.get(i);
920            if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
921                requestingServices++;
922            }
923
924            if (info.feedbackType != 0 && info.feedbackType !=
925                    AccessibilityServiceInfo.FEEDBACK_GENERIC) {
926                feedbackEnabled = true;
927            }
928        }
929
930        mAccessibilityFeedbackEnabled = feedbackEnabled;
931
932        final boolean showAccessibilityButton = requestingServices >= 1;
933        final boolean targetSelection = requestingServices >= 2;
934        mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection);
935    }
936
937    private void onRotateSuggestionClick(View v) {
938        mMetricsLogger.action(MetricsEvent.ACTION_ROTATION_SUGGESTION_ACCEPTED);
939        incrementNumAcceptedRotationSuggestionsIfNeeded();
940        mRotationLockController.setRotationLockedAtAngle(true, mLastRotationSuggestion);
941    }
942
943    private boolean onRotateSuggestionHover(View v, MotionEvent event) {
944        final int action = event.getActionMasked();
945        mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
946                || (action == MotionEvent.ACTION_HOVER_MOVE);
947        rescheduleRotationTimeout(true);
948        return false; // Must return false so a11y hover events are dispatched correctly.
949    }
950
951    // ----- Methods that StatusBar talks to (should be minimized) -----
952
953    public void setLightBarController(LightBarController lightBarController) {
954        mLightBarController = lightBarController;
955        mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController());
956    }
957
958    public boolean isSemiTransparent() {
959        return mNavigationBarMode == MODE_SEMI_TRANSPARENT;
960    }
961
962    public void disableAnimationsDuringHide(long delay) {
963        mNavigationBarView.setLayoutTransitionsEnabled(false);
964        mNavigationBarView.postDelayed(() -> mNavigationBarView.setLayoutTransitionsEnabled(true),
965                delay + StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
966    }
967
968    public BarTransitions getBarTransitions() {
969        return mNavigationBarView.getBarTransitions();
970    }
971
972    public void checkNavBarModes() {
973        mStatusBar.checkBarMode(mNavigationBarMode,
974                mNavigationBarWindowState, mNavigationBarView.getBarTransitions());
975    }
976
977    public void finishBarAnimations() {
978        mNavigationBarView.getBarTransitions().finishAnimations();
979    }
980
981    private final AccessibilityServicesStateChangeListener mAccessibilityListener =
982            this::updateAccessibilityServicesState;
983
984    private class MagnificationContentObserver extends ContentObserver {
985
986        public MagnificationContentObserver(Handler handler) {
987            super(handler);
988        }
989
990        @Override
991        public void onChange(boolean selfChange) {
992            NavigationBarFragment.this.updateAccessibilityServicesState(mAccessibilityManager);
993        }
994    }
995
996    private final Stub mRotationWatcher = new Stub() {
997        @Override
998        public void onRotationChanged(final int rotation) throws RemoteException {
999            // We need this to be scheduled as early as possible to beat the redrawing of
1000            // window in response to the orientation change.
1001            Handler h = getView().getHandler();
1002            Message msg = Message.obtain(h, () -> {
1003
1004                // If the screen rotation changes while locked, potentially update lock to flow with
1005                // new screen rotation and hide any showing suggestions.
1006                if (mRotationLockController.isRotationLocked()) {
1007                    if (shouldOverrideUserLockPrefs(rotation)) {
1008                        mRotationLockController.setRotationLockedAtAngle(true, rotation);
1009                    }
1010                    setRotateSuggestionButtonState(false, true);
1011                }
1012
1013                if (mNavigationBarView != null
1014                        && mNavigationBarView.needsReorient(rotation)) {
1015                    repositionNavigationBar();
1016                }
1017            });
1018            msg.setAsynchronous(true);
1019            h.sendMessageAtFrontOfQueue(msg);
1020        }
1021
1022        private boolean shouldOverrideUserLockPrefs(final int rotation) {
1023            // Only override user prefs when returning to the natural rotation (normally portrait).
1024            // Don't let apps that force landscape or 180 alter user lock.
1025            return rotation == NATURAL_ROTATION;
1026        }
1027    };
1028
1029    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
1030        @Override
1031        public void onReceive(Context context, Intent intent) {
1032            String action = intent.getAction();
1033            if (Intent.ACTION_SCREEN_OFF.equals(action)
1034                    || Intent.ACTION_SCREEN_ON.equals(action)) {
1035                notifyNavigationBarScreenOn();
1036            }
1037        }
1038    };
1039
1040    class TaskStackListenerImpl extends SysUiTaskStackChangeListener {
1041        // Invalidate any rotation suggestion on task change or activity orientation change
1042        // Note: all callbacks happen on main thread
1043
1044        @Override
1045        public void onTaskStackChanged() {
1046            setRotateSuggestionButtonState(false);
1047        }
1048
1049        @Override
1050        public void onTaskRemoved(int taskId) {
1051            setRotateSuggestionButtonState(false);
1052        }
1053
1054        @Override
1055        public void onTaskMovedToFront(int taskId) {
1056            setRotateSuggestionButtonState(false);
1057        }
1058
1059        @Override
1060        public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
1061            // Only hide the icon if the top task changes its requestedOrientation
1062            // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
1063            final boolean top = ActivityManagerWrapper.getInstance().getRunningTask().id == taskId;
1064            if (top) setRotateSuggestionButtonState(false);
1065        }
1066    }
1067
1068    private class ViewRippler {
1069        private static final int RIPPLE_OFFSET_MS = 50;
1070        private static final int RIPPLE_INTERVAL_MS = 2000;
1071        private View mRoot;
1072
1073        public void start(View root) {
1074            stop(); // Stop any pending ripple animations
1075
1076            mRoot = root;
1077
1078            // Schedule pending ripples, offset the 1st to avoid problems with visibility change
1079            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
1080            mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
1081            mRoot.postOnAnimationDelayed(mRipple, 2*RIPPLE_INTERVAL_MS);
1082        }
1083
1084        public void stop() {
1085            if (mRoot != null) mRoot.removeCallbacks(mRipple);
1086        }
1087
1088        private final Runnable mRipple = new Runnable() {
1089            @Override
1090            public void run() { // Cause the ripple to fire via false presses
1091                mRoot.setPressed(true);
1092                mRoot.setPressed(false);
1093            }
1094        };
1095    }
1096
1097    public static View create(Context context, FragmentListener listener) {
1098        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
1099                LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
1100                WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
1101                WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
1102                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
1103                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
1104                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
1105                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
1106                        | WindowManager.LayoutParams.FLAG_SLIPPERY,
1107                PixelFormat.TRANSLUCENT);
1108        lp.token = new Binder();
1109        lp.setTitle("NavigationBar");
1110        lp.accessibilityTitle = context.getString(R.string.nav_bar);
1111        lp.windowAnimations = 0;
1112
1113        View navigationBarView = LayoutInflater.from(context).inflate(
1114                R.layout.navigation_bar_window, null);
1115
1116        if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + navigationBarView);
1117        if (navigationBarView == null) return null;
1118
1119        context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
1120        FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);
1121        NavigationBarFragment fragment = new NavigationBarFragment();
1122        fragmentHost.getFragmentManager().beginTransaction()
1123                .replace(R.id.navigation_bar_frame, fragment, TAG)
1124                .commit();
1125        fragmentHost.addTagListener(TAG, listener);
1126        return navigationBarView;
1127    }
1128}
1129