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