/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.phone; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.DrawableRes; import android.app.StatusBarManager; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Region.Op; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dependency; import com.android.systemui.DockedStackExistsListener; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.SysUiServiceProvider; import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsOnboarding; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.systemui.statusbar.policy.DeadZone; import com.android.systemui.statusbar.policy.KeyButtonDrawable; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.function.Consumer; public class NavigationBarView extends FrameLayout implements NavigationModeController.ModeChangedListener { final static boolean DEBUG = false; final static String TAG = "StatusBar/NavBarView"; // slippery nav bar when everything is disabled, e.g. during setup final static boolean SLIPPERY_WHEN_DISABLED = true; final static boolean ALTERNATE_CAR_MODE_UI = false; View mCurrentView = null; private View mVertical; private View mHorizontal; /** Indicates that navigation bar is vertical. */ private boolean mIsVertical; private int mCurrentRotation = -1; boolean mLongClickableAccessibilityButton; int mDisabledFlags = 0; int mNavigationIconHints = 0; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; private Rect mHomeButtonBounds = new Rect(); private Rect mBackButtonBounds = new Rect(); private Rect mRecentsButtonBounds = new Rect(); private Rect mRotationButtonBounds = new Rect(); private final Region mActiveRegion = new Region(); private int[] mTmpPosition = new int[2]; private KeyButtonDrawable mBackIcon; private KeyButtonDrawable mHomeDefaultIcon; private KeyButtonDrawable mRecentIcon; private KeyButtonDrawable mDockedIcon; private final EdgeBackGestureHandler mEdgeBackGestureHandler; private final DeadZone mDeadZone; private boolean mDeadZoneConsuming = false; private final NavigationBarTransitions mBarTransitions; private final OverviewProxyService mOverviewProxyService; // performs manual animation in sync with layout transitions private final NavTransitionListener mTransitionListener = new NavTransitionListener(); private OnVerticalChangedListener mOnVerticalChangedListener; private boolean mLayoutTransitionsEnabled = true; private boolean mWakeAndUnlocking; private boolean mUseCarModeUi = false; private boolean mInCarMode = false; private boolean mDockedStackExists; private boolean mImeVisible; private final SparseArray mButtonDispatchers = new SparseArray<>(); private final ContextualButtonGroup mContextualButtonGroup; private Configuration mConfiguration; private Configuration mTmpLastConfiguration; private NavigationBarInflaterView mNavigationInflaterView; private RecentsOnboarding mRecentsOnboarding; private NotificationPanelView mPanelView; private FloatingRotationButton mFloatingRotationButton; private RotationButtonController mRotationButtonController; private NavBarTintController mTintController; /** * Helper that is responsible for showing the right toast when a disallowed activity operation * occurred. In pinned mode, we show instructions on how to break out of this mode, whilst in * fully locked mode we only show that unlocking is blocked. */ private ScreenPinningNotify mScreenPinningNotify; private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; private boolean mHomeAppearing; private long mStartDelay; private long mDuration; private TimeInterpolator mInterpolator; @Override public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (view.getId() == R.id.back) { mBackTransitioning = true; } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = true; mStartDelay = transition.getStartDelay(transitionType); mDuration = transition.getDuration(transitionType); mInterpolator = transition.getInterpolator(transitionType); } } @Override public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (view.getId() == R.id.back) { mBackTransitioning = false; } else if (view.getId() == R.id.home && transitionType == LayoutTransition.APPEARING) { mHomeAppearing = false; } } public void onBackAltCleared() { ButtonDispatcher backButton = getBackButton(); // When dismissing ime during unlock, force the back button to run the same appearance // animation as home (if we catch this condition early enough). if (!mBackTransitioning && backButton.getVisibility() == VISIBLE && mHomeAppearing && getHomeButton().getAlpha() == 0) { getBackButton().setAlpha(0); ValueAnimator a = ObjectAnimator.ofFloat(backButton, "alpha", 0, 1); a.setStartDelay(mStartDelay); a.setDuration(mDuration); a.setInterpolator(mInterpolator); a.start(); } } } private final OnClickListener mImeSwitcherClickListener = new OnClickListener() { @Override public void onClick(View view) { mContext.getSystemService(InputMethodManager.class).showInputMethodPickerFromSystem( true /* showAuxiliarySubtypes */, getContext().getDisplayId()); } }; private final AccessibilityDelegate mQuickStepAccessibilityDelegate = new AccessibilityDelegate() { private AccessibilityAction mToggleOverviewAction; @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); if (mToggleOverviewAction == null) { mToggleOverviewAction = new AccessibilityAction(R.id.action_toggle_overview, getContext().getString(R.string.quick_step_accessibility_toggle_overview)); } info.addAction(mToggleOverviewAction); } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == R.id.action_toggle_overview) { SysUiServiceProvider.getComponent(getContext(), Recents.class) .toggleRecentApps(); } else { return super.performAccessibilityAction(host, action, args); } return true; } }; private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> { // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully // gestural mode, the entire nav bar should be touchable. if (!isGesturalMode(mNavBarMode) || mImeVisible) { info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); ButtonDispatcher imeSwitchButton = getImeSwitchButton(); if (imeSwitchButton.getVisibility() == VISIBLE) { // If the IME is not up, but the ime switch button is visible, then make sure that // button is touchable int[] loc = new int[2]; View buttonView = imeSwitchButton.getCurrentView(); buttonView.getLocationInWindow(loc); info.touchableRegion.set(loc[0], loc[1], loc[0] + buttonView.getWidth(), loc[1] + buttonView.getHeight()); return; } info.touchableRegion.setEmpty(); }; public NavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); mIsVertical = false; mLongClickableAccessibilityButton = false; mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this); boolean isGesturalMode = isGesturalMode(mNavBarMode); // Set up the context group of buttons mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container); final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher, R.drawable.ic_ime_switcher_default); final RotationContextButton rotateSuggestionButton = new RotationContextButton( R.id.rotate_suggestion, R.drawable.ic_sysbar_rotate_button); final ContextualButton accessibilityButton = new ContextualButton(R.id.accessibility_button, R.drawable.ic_sysbar_accessibility_button); mContextualButtonGroup.addButton(imeSwitcherButton); if (!isGesturalMode) { mContextualButtonGroup.addButton(rotateSuggestionButton); } mContextualButtonGroup.addButton(accessibilityButton); mOverviewProxyService = Dependency.get(OverviewProxyService.class); mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService); mFloatingRotationButton = new FloatingRotationButton(context); mRotationButtonController = new RotationButtonController(context, R.style.RotateButtonCCWStart90, isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton); final ContextualButton backButton = new ContextualButton(R.id.back, 0); mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); mScreenPinningNotify = new ScreenPinningNotify(mContext); mBarTransitions = new NavigationBarTransitions(this); mButtonDispatchers.put(R.id.back, backButton); mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home)); mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle)); mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); mButtonDispatchers.put(R.id.ime_switcher, imeSwitcherButton); mButtonDispatchers.put(R.id.accessibility_button, accessibilityButton); mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton); mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup); mDeadZone = new DeadZone(this); mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService); mTintController = new NavBarTintController(this, getLightTransitionsController()); } public NavBarTintController getTintController() { return mTintController; } public NavigationBarTransitions getBarTransitions() { return mBarTransitions; } public LightBarTransitionsController getLightTransitionsController() { return mBarTransitions.getLightTransitionsController(); } public void setComponents(NotificationPanelView panel, AssistManager assistManager) { mPanelView = panel; updateSystemUiStateFlags(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mTintController.onDraw(); } public void setOnVerticalChangedListener(OnVerticalChangedListener onVerticalChangedListener) { mOnVerticalChangedListener = onVerticalChangedListener; notifyVerticalChangedListener(mIsVertical); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { shouldDeadZoneConsumeTouchEvents(event); return super.onTouchEvent(event); } void onBarTransition(int newMode) { if (newMode == MODE_OPAQUE) { // If the nav bar background is opaque, stop auto tinting since we know the icons are // showing over a dark background mTintController.stop(); getLightTransitionsController().setIconsDark(false /* dark */, true /* animate */); } else { mTintController.start(); } } private boolean shouldDeadZoneConsumeTouchEvents(MotionEvent event) { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { mDeadZoneConsuming = false; } if (mDeadZone.onTouchEvent(event) || mDeadZoneConsuming) { switch (action) { case MotionEvent.ACTION_DOWN: // Allow gestures starting in the deadzone to be slippery setSlippery(true); mDeadZoneConsuming = true; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // When a gesture started in the deadzone is finished, restore slippery state updateSlippery(); mDeadZoneConsuming = false; break; } return true; } return false; } public void abortCurrentGesture() { getHomeButton().abortCurrentGesture(); } public View getCurrentView() { return mCurrentView; } public RotationButtonController getRotationButtonController() { return mRotationButtonController; } public FloatingRotationButton getFloatingRotationButton() { return mFloatingRotationButton; } public ButtonDispatcher getRecentsButton() { return mButtonDispatchers.get(R.id.recent_apps); } public ButtonDispatcher getBackButton() { return mButtonDispatchers.get(R.id.back); } public ButtonDispatcher getHomeButton() { return mButtonDispatchers.get(R.id.home); } public ButtonDispatcher getImeSwitchButton() { return mButtonDispatchers.get(R.id.ime_switcher); } public ButtonDispatcher getAccessibilityButton() { return mButtonDispatchers.get(R.id.accessibility_button); } public RotationContextButton getRotateSuggestionButton() { return (RotationContextButton) mButtonDispatchers.get(R.id.rotate_suggestion); } public ButtonDispatcher getHomeHandle() { return mButtonDispatchers.get(R.id.home_handle); } public SparseArray getButtonDispatchers() { return mButtonDispatchers; } public boolean isRecentsButtonVisible() { return getRecentsButton().getVisibility() == View.VISIBLE; } public boolean isOverviewEnabled() { return (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) == 0; } public boolean isQuickStepSwipeUpEnabled() { return mOverviewProxyService.shouldShowSwipeUpUI() && isOverviewEnabled(); } private void reloadNavIcons() { updateIcons(Configuration.EMPTY); } private void updateIcons(Configuration oldConfig) { final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation; final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi; final boolean dirChange = oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection(); if (orientationChange || densityChange) { mDockedIcon = getDrawable(R.drawable.ic_sysbar_docked); mHomeDefaultIcon = getHomeDrawable(); } if (densityChange || dirChange) { mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent); mContextualButtonGroup.updateIcons(); } if (orientationChange || densityChange || dirChange) { mBackIcon = getBackDrawable(); } } public KeyButtonDrawable getBackDrawable() { KeyButtonDrawable drawable = getDrawable(getBackDrawableRes()); orientBackButton(drawable); return drawable; } public @DrawableRes int getBackDrawableRes() { return chooseNavigationIconDrawableRes(R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_quick_step); } public KeyButtonDrawable getHomeDrawable() { final boolean quickStepEnabled = mOverviewProxyService.shouldShowSwipeUpUI(); KeyButtonDrawable drawable = quickStepEnabled ? getDrawable(R.drawable.ic_sysbar_home_quick_step) : getDrawable(R.drawable.ic_sysbar_home); orientHomeButton(drawable); return drawable; } private void orientBackButton(KeyButtonDrawable drawable) { final boolean useAltBack = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; float degrees = useAltBack ? (isRtl ? 90 : -90) : 0; if (drawable.getRotation() == degrees) { return; } if (isGesturalMode(mNavBarMode)) { drawable.setRotation(degrees); return; } // Animate the back button's rotation to the new degrees and only in portrait move up the // back button to line up with the other buttons float targetY = !mOverviewProxyService.shouldShowSwipeUpUI() && !mIsVertical && useAltBack ? - getResources().getDimension(R.dimen.navbar_back_button_ime_offset) : 0; ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable, PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_ROTATE, degrees), PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_TRANSLATE_Y, targetY)); navBarAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); navBarAnimator.setDuration(200); navBarAnimator.start(); } private void orientHomeButton(KeyButtonDrawable drawable) { drawable.setRotation(mIsVertical ? 90 : 0); } private KeyButtonDrawable chooseNavigationIconDrawable(@DrawableRes int icon, @DrawableRes int quickStepIcon) { return getDrawable(chooseNavigationIconDrawableRes(icon, quickStepIcon)); } private @DrawableRes int chooseNavigationIconDrawableRes(@DrawableRes int icon, @DrawableRes int quickStepIcon) { final boolean quickStepEnabled = mOverviewProxyService.shouldShowSwipeUpUI(); return quickStepEnabled ? quickStepIcon : icon; } private KeyButtonDrawable getDrawable(@DrawableRes int icon) { return KeyButtonDrawable.create(mContext, icon, true /* hasShadow */); } private KeyButtonDrawable getDrawable(@DrawableRes int icon, boolean hasShadow) { return KeyButtonDrawable.create(mContext, icon, hasShadow); } public void setWindowVisible(boolean visible) { mTintController.setWindowVisible(visible); mRotationButtonController.onNavigationBarWindowVisibilityChange(visible); } @Override public void setLayoutDirection(int layoutDirection) { reloadNavIcons(); super.setLayoutDirection(layoutDirection); } public void setNavigationIconHints(int hints) { if (hints == mNavigationIconHints) return; final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; final boolean oldBackAlt = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; if (newBackAlt != oldBackAlt) { onImeVisibilityChanged(newBackAlt); } if (DEBUG) { android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500).show(); } mNavigationIconHints = hints; updateNavButtonIcons(); } private void onImeVisibilityChanged(boolean visible) { if (!visible) { mTransitionListener.onBackAltCleared(); } mImeVisible = visible; mRotationButtonController.getRotationButton().setCanShowRotationButton(!mImeVisible); } public void setDisabledFlags(int disabledFlags) { if (mDisabledFlags == disabledFlags) return; final boolean overviewEnabledBefore = isOverviewEnabled(); mDisabledFlags = disabledFlags; // Update icons if overview was just enabled to ensure the correct icons are present if (!overviewEnabledBefore && isOverviewEnabled()) { reloadNavIcons(); } updateNavButtonIcons(); updateSlippery(); setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled()); updateSystemUiStateFlags(); } public void updateNavButtonIcons() { // We have to replace or restore the back and home button icons when exiting or entering // carmode, respectively. Recents are not available in CarMode in nav bar so change // to recent icon is not required. final boolean useAltBack = (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0; KeyButtonDrawable backIcon = mBackIcon; orientBackButton(backIcon); KeyButtonDrawable homeIcon = mHomeDefaultIcon; if (!mUseCarModeUi) { orientHomeButton(homeIcon); } getHomeButton().setImageDrawable(homeIcon); getBackButton().setImageDrawable(backIcon); updateRecentsIcon(); // Update IME button visibility, a11y and rotate button always overrides the appearance mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0); mBarTransitions.reapplyDarkIntensity(); boolean disableHome = isGesturalMode(mNavBarMode) || ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); // Always disable recents when alternate car mode UI is active and for secondary displays. boolean disableRecent = isRecentsButtonDisabled(); boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode) || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)); // When screen pinning, don't hide back and home when connected service or back and // recents buttons when disconnected from launcher service in screen pinning mode, // as they are used for exiting. final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); if (mOverviewProxyService.isEnabled()) { // Force disable recents when not in legacy mode disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode); if (pinningActive) { disableBack = disableHome = false; } } else if (pinningActive) { disableBack = disableRecent = false; } ViewGroup navButtons = getCurrentView().findViewById(R.id.nav_buttons); if (navButtons != null) { LayoutTransition lt = navButtons.getLayoutTransition(); if (lt != null) { if (!lt.getTransitionListeners().contains(mTransitionListener)) { lt.addTransitionListener(mTransitionListener); } } } getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); } @VisibleForTesting boolean isRecentsButtonDisabled() { return mUseCarModeUi || !isOverviewEnabled() || getContext().getDisplayId() != Display.DEFAULT_DISPLAY; } private Display getContextDisplay() { return getContext().getDisplay(); } public void setLayoutTransitionsEnabled(boolean enabled) { mLayoutTransitionsEnabled = enabled; updateLayoutTransitionsEnabled(); } public void setWakeAndUnlocking(boolean wakeAndUnlocking) { setUseFadingAnimations(wakeAndUnlocking); mWakeAndUnlocking = wakeAndUnlocking; updateLayoutTransitionsEnabled(); } private void updateLayoutTransitionsEnabled() { boolean enabled = !mWakeAndUnlocking && mLayoutTransitionsEnabled; ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons); LayoutTransition lt = navButtons.getLayoutTransition(); if (lt != null) { if (enabled) { lt.enableTransitionType(LayoutTransition.APPEARING); lt.enableTransitionType(LayoutTransition.DISAPPEARING); lt.enableTransitionType(LayoutTransition.CHANGE_APPEARING); lt.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); } else { lt.disableTransitionType(LayoutTransition.APPEARING); lt.disableTransitionType(LayoutTransition.DISAPPEARING); lt.disableTransitionType(LayoutTransition.CHANGE_APPEARING); lt.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); } } } private void setUseFadingAnimations(boolean useFadingAnimations) { WindowManager.LayoutParams lp = (WindowManager.LayoutParams) ((ViewGroup) getParent()) .getLayoutParams(); if (lp != null) { boolean old = lp.windowAnimations != 0; if (!old && useFadingAnimations) { lp.windowAnimations = R.style.Animation_NavigationBarFadeIn; } else if (old && !useFadingAnimations) { lp.windowAnimations = 0; } else { return; } WindowManager wm = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE); wm.updateViewLayout((View) getParent(), lp); } } public void onPanelExpandedChange() { updateSlippery(); updateSystemUiStateFlags(); } public void updateSystemUiStateFlags() { int displayId = mContext.getDisplayId(); mOverviewProxyService.setSystemUiStateFlag(SYSUI_STATE_SCREEN_PINNING, ActivityManagerWrapper.getInstance().isScreenPinningActive(), displayId); mOverviewProxyService.setSystemUiStateFlag(SYSUI_STATE_OVERVIEW_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0, displayId); mOverviewProxyService.setSystemUiStateFlag(SYSUI_STATE_HOME_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0, displayId); if (mPanelView != null) { mOverviewProxyService.setSystemUiStateFlag(SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED, mPanelView.isFullyExpanded() && !mPanelView.isInSettings(), displayId); } } public void updateStates() { final boolean showSwipeUpUI = mOverviewProxyService.shouldShowSwipeUpUI(); if (mNavigationInflaterView != null) { // Reinflate the navbar if needed, no-op unless the swipe up state changes mNavigationInflaterView.onLikelyDefaultLayoutChange(); } updateSlippery(); reloadNavIcons(); updateNavButtonIcons(); setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled()); WindowManagerWrapper.getInstance().setNavBarVirtualKeyHapticFeedbackEnabled(!showSwipeUpUI); getHomeButton().setAccessibilityDelegate( showSwipeUpUI ? mQuickStepAccessibilityDelegate : null); } /** * Updates the {@link WindowManager.LayoutParams.FLAG_SLIPPERY} state dependent on if swipe up * is enabled, or the notifications is fully opened without being in an animated state. If * slippery is enabled, touch events will leave the nav bar window and enter into the fullscreen * app/home window, if not nav bar will receive a cancelled touch event once gesture leaves bar. */ public void updateSlippery() { setSlippery(!isQuickStepSwipeUpEnabled() || (mPanelView.isFullyExpanded() && !mPanelView.isCollapsing())); } private void setSlippery(boolean slippery) { setWindowFlag(WindowManager.LayoutParams.FLAG_SLIPPERY, slippery); } private void setWindowFlag(int flags, boolean enable) { final ViewGroup navbarView = ((ViewGroup) getParent()); if (navbarView == null) { return; } WindowManager.LayoutParams lp = (WindowManager.LayoutParams) navbarView.getLayoutParams(); if (lp == null || enable == ((lp.flags & flags) != 0)) { return; } if (enable) { lp.flags |= flags; } else { lp.flags &= ~flags; } WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); wm.updateViewLayout(navbarView, lp); } @Override public void onNavigationModeChanged(int mode) { Context curUserCtx = Dependency.get(NavigationModeController.class).getCurrentUserContext(); mNavBarMode = mode; mBarTransitions.onNavigationModeChanged(mNavBarMode); mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode, curUserCtx); mRecentsOnboarding.onNavigationModeChanged(mNavBarMode); getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode); // Color adaption is tied with showing home handle, only avaliable if visible mTintController.onNavigationModeChanged(mNavBarMode); if (isGesturalMode(mNavBarMode)) { mTintController.start(); } else { mTintController.stop(); } } public void setAccessibilityButtonState(final boolean visible, final boolean longClickable) { mLongClickableAccessibilityButton = longClickable; getAccessibilityButton().setLongClickable(longClickable); mContextualButtonGroup.setButtonVisibility(R.id.accessibility_button, visible); } void hideRecentsOnboarding() { mRecentsOnboarding.hide(true); } @Override public void onFinishInflate() { mNavigationInflaterView = findViewById(R.id.navigation_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener); DockedStackExistsListener.register(mDockedListener); updateOrientationViews(); reloadNavIcons(); } @Override protected void onDraw(Canvas canvas) { mDeadZone.onDraw(canvas); super.onDraw(canvas); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mActiveRegion.setEmpty(); updateButtonLocation(getBackButton(), mBackButtonBounds, true); updateButtonLocation(getHomeButton(), mHomeButtonBounds, false); updateButtonLocation(getRecentsButton(), mRecentsButtonBounds, false); updateButtonLocation(getRotateSuggestionButton(), mRotationButtonBounds, true); // TODO: Handle button visibility changes mOverviewProxyService.onActiveNavBarRegionChanges(mActiveRegion); mRecentsOnboarding.setNavBarHeight(getMeasuredHeight()); } private void updateButtonLocation(ButtonDispatcher button, Rect buttonBounds, boolean isActive) { View view = button.getCurrentView(); if (view == null) { buttonBounds.setEmpty(); return; } // Temporarily reset the translation back to origin to get the position in window final float posX = view.getTranslationX(); final float posY = view.getTranslationY(); view.setTranslationX(0); view.setTranslationY(0); if (isActive) { view.getLocationOnScreen(mTmpPosition); buttonBounds.set(mTmpPosition[0], mTmpPosition[1], mTmpPosition[0] + view.getMeasuredWidth(), mTmpPosition[1] + view.getMeasuredHeight()); mActiveRegion.op(buttonBounds, Op.UNION); } view.getLocationInWindow(mTmpPosition); buttonBounds.set(mTmpPosition[0], mTmpPosition[1], mTmpPosition[0] + view.getMeasuredWidth(), mTmpPosition[1] + view.getMeasuredHeight()); view.setTranslationX(posX); view.setTranslationY(posY); } private void updateOrientationViews() { mHorizontal = findViewById(R.id.horizontal); mVertical = findViewById(R.id.vertical); updateCurrentView(); } boolean needsReorient(int rotation) { return mCurrentRotation != rotation; } private void updateCurrentView() { resetViews(); mCurrentView = mIsVertical ? mVertical : mHorizontal; mCurrentView.setVisibility(View.VISIBLE); mNavigationInflaterView.setVertical(mIsVertical); mCurrentRotation = getContextDisplay().getRotation(); mNavigationInflaterView.setAlternativeOrder(mCurrentRotation == Surface.ROTATION_90); mNavigationInflaterView.updateButtonDispatchersCurrentView(); updateLayoutTransitionsEnabled(); } private void resetViews() { mHorizontal.setVisibility(View.GONE); mVertical.setVisibility(View.GONE); } private void updateRecentsIcon() { mDockedIcon.setRotation(mDockedStackExists && mIsVertical ? 90 : 0); getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon); mBarTransitions.reapplyDarkIntensity(); } public void showPinningEnterExitToast(boolean entering) { if (entering) { mScreenPinningNotify.showPinningStartToast(); } else { mScreenPinningNotify.showPinningExitToast(); } } public void showPinningEscapeToast() { mScreenPinningNotify.showEscapeToast(isRecentsButtonVisible()); } public boolean isVertical() { return mIsVertical; } public void reorient() { updateCurrentView(); ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone); mDeadZone.onConfigurationChanged(mCurrentRotation); // force the low profile & disabled states into compliance mBarTransitions.init(); if (DEBUG) { Log.d(TAG, "reorient(): rot=" + mCurrentRotation); } // Resolve layout direction if not resolved since components changing layout direction such // as changing languages will recreate this view and the direction will be resolved later if (!isLayoutDirectionResolved()) { resolveLayoutDirection(); } updateNavButtonIcons(); getHomeButton().setVertical(mIsVertical); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); if (DEBUG) Log.d(TAG, String.format( "onMeasure: (%dx%d) old: (%dx%d)", w, h, getMeasuredWidth(), getMeasuredHeight())); final boolean newVertical = w > 0 && h > w && !isGesturalMode(mNavBarMode); if (newVertical != mIsVertical) { mIsVertical = newVertical; if (DEBUG) { Log.d(TAG, String.format("onMeasure: h=%d, w=%d, vert=%s", h, w, mIsVertical ? "y" : "n")); } reorient(); notifyVerticalChangedListener(newVertical); } if (isGesturalMode(mNavBarMode)) { // Update the nav bar background to match the height of the visible nav bar int height = mIsVertical ? getResources().getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_height_landscape) : getResources().getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_height); int frameHeight = getResources().getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_frame_height); mBarTransitions.setBackgroundFrame(new Rect(0, frameHeight - height, w, h)); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private void notifyVerticalChangedListener(boolean newVertical) { if (mOnVerticalChangedListener != null) { mOnVerticalChangedListener.onVerticalChanged(newVertical); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mTmpLastConfiguration.updateFrom(mConfiguration); mConfiguration.updateFrom(newConfig); boolean uiCarModeChanged = updateCarMode(); updateIcons(mTmpLastConfiguration); updateRecentsIcon(); mRecentsOnboarding.onConfigurationChanged(mConfiguration); if (uiCarModeChanged || mTmpLastConfiguration.densityDpi != mConfiguration.densityDpi || mTmpLastConfiguration.getLayoutDirection() != mConfiguration.getLayoutDirection()) { // If car mode or density changes, we need to reset the icons. updateNavButtonIcons(); } } /** * If the configuration changed, update the carmode and return that it was updated. */ private boolean updateCarMode() { boolean uiCarModeChanged = false; if (mConfiguration != null) { int uiMode = mConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK; final boolean isCarMode = (uiMode == Configuration.UI_MODE_TYPE_CAR); if (isCarMode != mInCarMode) { mInCarMode = isCarMode; if (ALTERNATE_CAR_MODE_UI) { mUseCarModeUi = isCarMode; uiCarModeChanged = true; } else { // Don't use car mode behavior if ALTERNATE_CAR_MODE_UI not set. mUseCarModeUi = false; } } } return uiCarModeChanged; } private String getResourceName(int resId) { if (resId != 0) { final android.content.res.Resources res = getContext().getResources(); try { return res.getResourceName(resId); } catch (android.content.res.Resources.NotFoundException ex) { return "(unknown)"; } } else { return "(null)"; } } private static String visibilityToString(int vis) { switch (vis) { case View.INVISIBLE: return "INVISIBLE"; case View.GONE: return "GONE"; default: return "VISIBLE"; } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); requestApplyInsets(); reorient(); onNavigationModeChanged(mNavBarMode); setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled()); if (mRotationButtonController != null) { mRotationButtonController.registerListeners(); } mEdgeBackGestureHandler.onNavBarAttached(); getViewTreeObserver().addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); Dependency.get(NavigationModeController.class).removeListener(this); setUpSwipeUpOnboarding(false); for (int i = 0; i < mButtonDispatchers.size(); ++i) { mButtonDispatchers.valueAt(i).onDestroy(); } if (mRotationButtonController != null) { mRotationButtonController.unregisterListeners(); } mEdgeBackGestureHandler.onNavBarDetached(); getViewTreeObserver().removeOnComputeInternalInsetsListener( mOnComputeInternalInsetsListener); } private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) { if (connectedToOverviewProxy) { mRecentsOnboarding.onConnectedToLauncher(); } else { mRecentsOnboarding.onDisconnectedFromLauncher(); } } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NavigationBarView {"); final Rect r = new Rect(); final Point size = new Point(); getContextDisplay().getRealSize(size); pw.println(String.format(" this: " + StatusBar.viewInfo(this) + " " + visibilityToString(getVisibility()))); getWindowVisibleDisplayFrame(r); final boolean offscreen = r.right > size.x || r.bottom > size.y; pw.println(" window: " + r.toShortString() + " " + visibilityToString(getWindowVisibility()) + (offscreen ? " OFFSCREEN!" : "")); pw.println(String.format(" mCurrentView: id=%s (%dx%d) %s %f", getResourceName(getCurrentView().getId()), getCurrentView().getWidth(), getCurrentView().getHeight(), visibilityToString(getCurrentView().getVisibility()), getCurrentView().getAlpha())); pw.println(String.format(" disabled=0x%08x vertical=%s darkIntensity=%.2f", mDisabledFlags, mIsVertical ? "true" : "false", getLightTransitionsController().getCurrentDarkIntensity())); dumpButton(pw, "back", getBackButton()); dumpButton(pw, "home", getHomeButton()); dumpButton(pw, "rcnt", getRecentsButton()); dumpButton(pw, "rota", getRotateSuggestionButton()); dumpButton(pw, "a11y", getAccessibilityButton()); pw.println(" }"); mContextualButtonGroup.dump(pw); mRecentsOnboarding.dump(pw); mTintController.dump(pw); mEdgeBackGestureHandler.dump(pw); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { int leftInset = insets.getSystemWindowInsetLeft(); int rightInset = insets.getSystemWindowInsetRight(); setPadding(leftInset, insets.getSystemWindowInsetTop(), rightInset, insets.getSystemWindowInsetBottom()); // we're passing the insets onto the gesture handler since the back arrow is only // conditionally added and doesn't always get all the insets. mEdgeBackGestureHandler.setInsets(leftInset, rightInset); return super.onApplyWindowInsets(insets); } private static void dumpButton(PrintWriter pw, String caption, ButtonDispatcher button) { pw.print(" " + caption + ": "); if (button == null) { pw.print("null"); } else { pw.print(visibilityToString(button.getVisibility()) + " alpha=" + button.getAlpha() ); } pw.println(); } public interface OnVerticalChangedListener { void onVerticalChanged(boolean isVertical); } private final Consumer mDockedListener = exists -> post(() -> { mDockedStackExists = exists; updateRecentsIcon(); }); }