/* * Copyright (C) 2021 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.launcher3.taskbar; import static android.view.KeyEvent.ACTION_UP; import static android.view.View.AccessibilityDelegate; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS; import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_SPACE; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_KEYGUARD; import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_SMALL_SCREEN; import static com.android.launcher3.util.FlagDebugUtils.appendFlag; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BACK_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; 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_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING; import static com.android.window.flags.Flags.predictiveBackThreeButtonNav; import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar; import android.animation.Animator; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.DrawableRes; import android.annotation.IdRes; import android.annotation.LayoutRes; import android.content.Context; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.graphics.drawable.PaintDrawable; import android.graphics.drawable.RotateDrawable; import android.inputmethodservice.InputMethodService; import android.os.Handler; import android.util.Property; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.inputmethod.Flags; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Space; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AlphaUpdateListener; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton; import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory; import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter; import com.android.launcher3.taskbar.navbutton.NearestTouchFrame; import com.android.launcher3.util.DimensionUtils; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.window.WindowManagerProxy; import com.android.launcher3.views.BaseDragLayer; import com.android.systemui.shared.navigationbar.KeyButtonRipple; import com.android.systemui.shared.rotation.FloatingRotationButton; import com.android.systemui.shared.rotation.RotationButton; import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.ArrayList; import java.util.StringJoiner; import java.util.function.IntPredicate; /** * Controller for managing nav bar buttons in taskbar */ public class NavbarButtonsViewController implements TaskbarControllers.LoggableTaskbarController, BubbleBarController.BubbleBarLocationListener { private final Rect mTempRect = new Rect(); private static final int FLAG_SWITCHER_SHOWING = 1 << 0; private static final int FLAG_IME_VISIBLE = 1 << 1; private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2; private static final int FLAG_A11Y_VISIBLE = 1 << 3; private static final int FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE = 1 << 4; private static final int FLAG_KEYGUARD_VISIBLE = 1 << 5; private static final int FLAG_KEYGUARD_OCCLUDED = 1 << 6; private static final int FLAG_DISABLE_HOME = 1 << 7; private static final int FLAG_DISABLE_RECENTS = 1 << 8; private static final int FLAG_DISABLE_BACK = 1 << 9; private static final int FLAG_NOTIFICATION_SHADE_EXPANDED = 1 << 10; private static final int FLAG_SCREEN_PINNING_ACTIVE = 1 << 11; private static final int FLAG_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 12; private static final int FLAG_SMALL_SCREEN = 1 << 13; private static final int FLAG_SLIDE_IN_VIEW_VISIBLE = 1 << 14; private static final int FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING = 1 << 15; /** * Flags where a UI could be over Taskbar surfaces, so the color override should be disabled. */ private static final int FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_VOICE_INTERACTION_WINDOW_SHOWING; private static final String NAV_BUTTONS_SEPARATE_WINDOW_TITLE = "Taskbar Nav Buttons"; private static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95; private static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05; public static final int ALPHA_INDEX_IMMERSIVE_MODE = 0; public static final int ALPHA_INDEX_KEYGUARD_OR_DISABLE = 1; public static final int ALPHA_INDEX_SUW = 2; private static final int NUM_ALPHA_CHANNELS = 3; private static final long AUTODIM_TIMEOUT_MS = 2250; private final ArrayList mPropertyHolders = new ArrayList<>(); private final ArrayList mAllButtons = new ArrayList<>(); private int mState; private final TaskbarActivityContext mContext; private final @Nullable Context mNavigationBarPanelContext; private final WindowManagerProxy mWindowManagerProxy; private final NearestTouchFrame mNavButtonsView; private final Handler mHandler; private final LinearLayout mNavButtonContainer; // Used for IME+A11Y buttons private final ViewGroup mEndContextualContainer; private final ViewGroup mStartContextualContainer; private final int mLightIconColorOnWorkspace; private final int mDarkIconColorOnWorkspace; /** Color to use for navbar buttons, if they are on on a Taskbar surface background. */ private final int mOnBackgroundIconColor; private @Nullable Animator mNavBarLocationAnimator; private @Nullable BubbleBarLocation mBubbleBarTargetLocation; private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat( this::updateNavButtonTranslationY); private final AnimatedFloat mTaskbarNavButtonTranslationYForIme = new AnimatedFloat( this::updateNavButtonTranslationY); private float mLastSetNavButtonTranslationY; // Used for System UI state updates that should translate the nav button for in-app display. private final AnimatedFloat mNavButtonInAppDisplayProgressForSysui = new AnimatedFloat( this::updateNavButtonInAppDisplayProgressForSysui); /** * Expected nav button dark intensity piped down from {@code LightBarController} in framework * via {@code TaskbarDelegate}. */ private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat( this::onDarkIntensityChanged); /** {@code 1} if the Taskbar background color is fully opaque. */ private final AnimatedFloat mOnTaskbarBackgroundNavButtonColorOverride = new AnimatedFloat( this::updateNavButtonColor); /** {@code 1} if a Taskbar slide in overlay is visible over Taskbar. */ private final AnimatedFloat mSlideInViewVisibleNavButtonColorOverride = new AnimatedFloat( this::updateNavButtonColor); /** Disables the {@link #mOnBackgroundIconColor} override if {@code 0}. */ private final AnimatedFloat mOnBackgroundNavButtonColorOverrideMultiplier = new AnimatedFloat( this::updateNavButtonColor); private final RotationButtonListener mRotationButtonListener = new RotationButtonListener(); private final Rect mFloatingRotationButtonBounds = new Rect(); // Initialized in init. private TaskbarControllers mControllers; private boolean mIsImeRenderingNavButtons; private ImageView mA11yButton; @SystemUiStateFlags private long mSysuiStateFlags; private ImageView mBackButton; private ImageView mHomeButton; private MultiValueAlpha mBackButtonAlpha; private MultiValueAlpha mHomeButtonAlpha; private FloatingRotationButton mFloatingRotationButton; private ImageView mImeSwitcherButton; // Variables for moving nav buttons to a separate window above IME private boolean mAreNavButtonsInSeparateWindow = false; private BaseDragLayer mSeparateWindowParent; // Initialized in init. private final ViewTreeObserver.OnComputeInternalInsetsListener mSeparateWindowInsetsComputer = this::onComputeInsetsForSeparateWindow; private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender(); private ImageView mRecentsButton; private Space mSpace; private TaskbarTransitions mTaskbarTransitions; private @BarTransitions.TransitionMode int mTransitionMode; private final Runnable mAutoDim = () -> mTaskbarTransitions.setAutoDim(true); public NavbarButtonsViewController(TaskbarActivityContext context, @Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView, Handler handler) { mContext = context; mNavigationBarPanelContext = navigationBarPanelContext; mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext); mNavButtonsView = navButtonsView; mHandler = handler; mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons); mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons); mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons); mLightIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_light_color_on_home); mDarkIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home); mOnBackgroundIconColor = Utilities.isDarkTheme(context) ? context.getColor(R.color.taskbar_nav_icon_light_color) : context.getColor(R.color.taskbar_nav_icon_dark_color); if (mContext.isPhoneMode()) { mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView); } } /** * Initializes the controller */ public void init(TaskbarControllers controllers) { mControllers = controllers; setupController(); } protected void setupController() { boolean isThreeButtonNav = mContext.isThreeButtonNav(); DeviceProfile deviceProfile = mContext.getDeviceProfile(); Resources resources = mContext.getResources(); int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize(); Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources, mContext.isPhoneMode(), mContext.isGestureNav()); ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams(); navButtonsViewLayoutParams.width = p.x; if (!mContext.isUserSetupComplete()) { // Setup mode in phone mode uses gesture nav. navButtonsViewLayoutParams.height = setupSize; } else { navButtonsViewLayoutParams.height = p.y; } mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams); mIsImeRenderingNavButtons = InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar(); if (!mIsImeRenderingNavButtons) { // IME switcher final int switcherResId = Flags.imeSwitcherRevamp() ? com.android.internal.R.drawable.ic_ime_switcher_new : R.drawable.ic_ime_switcher; mImeSwitcherButton = addButton(switcherResId, BUTTON_IME_SWITCH, isThreeButtonNav ? mStartContextualContainer : mEndContextualContainer, mControllers.navButtonController, R.id.ime_switcher); mPropertyHolders.add(new StatePropertyHolder(mImeSwitcherButton, flags -> ((flags & FLAG_SWITCHER_SHOWING) != 0) && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0))); } mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarViewController.getTaskbarIconAlpha() .get(ALPHA_INDEX_KEYGUARD), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_SCREEN_PINNING_ACTIVE) == 0)); mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarViewController.getTaskbarIconAlpha() .get(ALPHA_INDEX_SMALL_SCREEN), flags -> (flags & FLAG_SMALL_SCREEN) == 0)); if (!mContext.isPhoneMode()) { mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0)); } // Start at 1 because relevant flags are unset at init. mOnBackgroundNavButtonColorOverrideMultiplier.value = 1; // Force nav buttons (specifically back button) to be visible during setup wizard. boolean isInSetup = !mContext.isUserSetupComplete(); boolean isInKidsMode = mContext.isNavBarKidsModeActive(); boolean alwaysShowButtons = isThreeButtonNav || isInSetup; // Make sure to remove nav bar buttons translation when any of the following occur: // - Notification shade is expanded // - IME is showing (add separate translation for IME) // - VoiceInteractionWindow (assistant) is showing // - Keyboard shortcuts helper is showing if (!mContext.isPhoneMode()) { int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING; mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui, flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE, 1, 0)); // Center nav buttons in new height for IME. float transForIme = (mContext.getDeviceProfile().taskbarHeight - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f; // For gesture nav, nav buttons only show for IME anyway so keep them translated down. float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme; mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme, flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE, transForIme, defaultButtonTransY)); mPropertyHolders.add(new StatePropertyHolder( mOnBackgroundNavButtonColorOverrideMultiplier, flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0)); mPropertyHolders.add(new StatePropertyHolder( mSlideInViewVisibleNavButtonColorOverride, flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0)); } if (alwaysShowButtons) { initButtons(mNavButtonContainer, mEndContextualContainer, mControllers.navButtonController); updateButtonLayoutSpacing(); updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode()); if (!mContext.isPhoneMode()) { mPropertyHolders.add(new StatePropertyHolder( mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(), flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0)); } } else if (!mIsImeRenderingNavButtons) { View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mStartContextualContainer, mControllers.navButtonController, R.id.back); imeDownButton.setRotation(Utilities.isRtl(resources) ? 90 : -90); // Only show when IME is visible. mPropertyHolders.add(new StatePropertyHolder(imeDownButton, flags -> (flags & FLAG_IME_VISIBLE) != 0)); } mFloatingRotationButton = new FloatingRotationButton( ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext, R.string.accessibility_rotate_button, R.layout.rotate_suggestion, R.id.rotate_suggestion, R.dimen.floating_rotation_button_min_margin, R.dimen.rounded_corner_content_padding, R.dimen.floating_rotation_button_taskbar_left_margin, R.dimen.floating_rotation_button_taskbar_bottom_margin, R.dimen.floating_rotation_button_diameter, R.dimen.key_button_ripple_max_width, R.bool.floating_rotation_button_position_left); mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton, mRotationButtonListener); if (mContext.isPhoneMode()) { mTaskbarTransitions.init(); } applyState(); mPropertyHolders.forEach(StatePropertyHolder::endAnimation); // Initialize things needed to move nav buttons to separate window. mSeparateWindowParent = new BaseDragLayer<>(mContext, null, 0) { @Override public void recreateControllers() { mControllers = new TouchController[0]; } @Override protected boolean canFindActiveController() { // We don't have any controllers, but we don't want any floating views such as // folder to intercept, either. This ensures nav buttons can always be pressed. return false; } }; mSeparateWindowParent.recreateControllers(); if (BubbleBarController.isBubbleBarEnabled()) { mNavButtonsView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> onLayoutsUpdated() ); } } private void initButtons(ViewGroup navContainer, ViewGroup endContainer, TaskbarNavButtonController navButtonController) { mBackButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK, mNavButtonContainer, mControllers.navButtonController, R.id.back); mBackButtonAlpha = new MultiValueAlpha(mBackButton, NUM_ALPHA_CHANNELS); mBackButtonAlpha.setUpdateVisibility(true); mPropertyHolders.add(new StatePropertyHolder( mBackButtonAlpha.get(ALPHA_INDEX_KEYGUARD_OR_DISABLE), flags -> { // Show only if not disabled, and if not on the keyguard or otherwise only when // the bouncer or a lockscreen app is showing above the keyguard boolean showingOnKeyguard = (flags & FLAG_KEYGUARD_VISIBLE) == 0 || (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 || (flags & FLAG_KEYGUARD_OCCLUDED) != 0; return (flags & FLAG_DISABLE_BACK) == 0 && (!mContext.isGestureNav() || !mContext.isUserSetupComplete()) && ((flags & FLAG_KEYGUARD_VISIBLE) == 0 || showingOnKeyguard); })); // Hide back button in SUW if keyboard is showing (IME draws its own back). if (mIsImeRenderingNavButtons) { mPropertyHolders.add(new StatePropertyHolder( mBackButtonAlpha.get(ALPHA_INDEX_SUW), flags -> (flags & FLAG_IME_VISIBLE) == 0)); } mPropertyHolders.add(new StatePropertyHolder(mBackButton, flags -> (flags & FLAG_IME_VISIBLE) != 0, ROTATION_DRAWABLE_PERCENT, 1f, 0f)); // Translate back button to be at end/start of other buttons for keyguard (only after SUW // since it is laid to align with SUW actions while in that state) int navButtonSize = mContext.getResources().getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size); boolean isRtl = Utilities.isRtl(mContext.getResources()); if (!mContext.isPhoneMode()) { mPropertyHolders.add(new StatePropertyHolder( mBackButton, flags -> mContext.isUserSetupComplete() && ((flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0 || (flags & FLAG_KEYGUARD_VISIBLE) != 0), VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0)); } // home button mHomeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer, navButtonController, R.id.home); mHomeButtonAlpha = new MultiValueAlpha(mHomeButton, NUM_ALPHA_CHANNELS); mHomeButtonAlpha.setUpdateVisibility(true); mPropertyHolders.add( new StatePropertyHolder(mHomeButtonAlpha.get( ALPHA_INDEX_KEYGUARD_OR_DISABLE), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_HOME) == 0 && !mContext.isGestureNav())); // Recents button mRecentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS, navContainer, navButtonController, R.id.recent_apps); mHitboxExtender.init(mRecentsButton, mNavButtonsView, mContext.getDeviceProfile(), () -> { float[] recentsCoords = new float[2]; getDescendantCoordRelativeToAncestor(mRecentsButton, mNavButtonsView, recentsCoords, false); return recentsCoords; }, new Handler()); mRecentsButton.setOnClickListener(v -> { navButtonController.onButtonClick(BUTTON_RECENTS, v); mHitboxExtender.onRecentsButtonClicked(); }); mPropertyHolders.add(new StatePropertyHolder(mRecentsButton, flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0 && !mContext.isNavBarKidsModeActive() && !mContext.isGestureNav())); // A11y button mA11yButton = addButton(R.drawable.ic_sysbar_accessibility_button, BUTTON_A11Y, endContainer, navButtonController, R.id.accessibility_button, R.layout.taskbar_contextual_button); mPropertyHolders.add(new StatePropertyHolder(mA11yButton, flags -> (flags & FLAG_A11Y_VISIBLE) != 0 && (flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)); mSpace = new Space(mNavButtonsView.getContext()); mSpace.setOnClickListener(view -> navButtonController.onButtonClick(BUTTON_SPACE, view)); mSpace.setOnLongClickListener(view -> navButtonController.onButtonLongClick(BUTTON_SPACE, view)); } private void parseSystemUiFlags(@SystemUiStateFlags long sysUiStateFlags) { mSysuiStateFlags = sysUiStateFlags; boolean isImeVisible = (sysUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0; boolean isImeSwitcherShowing = (sysUiStateFlags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0; boolean a11yVisible = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0; boolean isHomeDisabled = (sysUiStateFlags & SYSUI_STATE_HOME_DISABLED) != 0; boolean isRecentsDisabled = (sysUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0; boolean isBackDisabled = (sysUiStateFlags & SYSUI_STATE_BACK_DISABLED) != 0; long shadeExpandedFlags = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; boolean isNotificationShadeExpanded = (sysUiStateFlags & shadeExpandedFlags) != 0; boolean isScreenPinningActive = (sysUiStateFlags & SYSUI_STATE_SCREEN_PINNING) != 0; boolean isVoiceInteractionWindowShowing = (sysUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0; boolean isKeyboardShortcutHelperShowing = (sysUiStateFlags & SYSUI_STATE_SHORTCUT_HELPER_SHOWING) != 0; // TODO(b/202218289) we're getting IME as not visible on lockscreen from system updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible); updateStateForFlag(FLAG_SWITCHER_SHOWING, isImeSwitcherShowing); updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible); updateStateForFlag(FLAG_DISABLE_HOME, isHomeDisabled); updateStateForFlag(FLAG_DISABLE_RECENTS, isRecentsDisabled); updateStateForFlag(FLAG_DISABLE_BACK, isBackDisabled); updateStateForFlag(FLAG_NOTIFICATION_SHADE_EXPANDED, isNotificationShadeExpanded); updateStateForFlag(FLAG_SCREEN_PINNING_ACTIVE, isScreenPinningActive); updateStateForFlag(FLAG_VOICE_INTERACTION_WINDOW_SHOWING, isVoiceInteractionWindowShowing); updateStateForFlag(FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING, isKeyboardShortcutHelperShowing); if (mA11yButton != null) { // Only used in 3 button boolean a11yLongClickable = (sysUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0; mA11yButton.setLongClickable(a11yLongClickable); updateButtonLayoutSpacing(); } } public void updateStateForSysuiFlags(@SystemUiStateFlags long systemUiStateFlags, boolean skipAnim) { if (systemUiStateFlags == mSysuiStateFlags) { return; } parseSystemUiFlags(systemUiStateFlags); applyState(); if (skipAnim) { mPropertyHolders.forEach(StatePropertyHolder::endAnimation); } } /** * @return {@code true} if A11y is showing in 3 button nav taskbar */ private boolean isA11yButtonPersistent() { return mContext.isThreeButtonNav() && (mState & FLAG_A11Y_VISIBLE) != 0; } /** * Should be called when we need to show back button for bouncer */ public void setBackForBouncer(boolean isBouncerVisible) { updateStateForFlag(FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, isBouncerVisible); applyState(); } /** * Slightly misnamed, but should be called when keyguard OR AOD is showing. * We consider keyguardVisible when it's showing bouncer OR is occlucded by another app */ public void setKeyguardVisible(boolean isKeyguardVisible, boolean isKeyguardOccluded) { updateStateForFlag(FLAG_KEYGUARD_VISIBLE, isKeyguardVisible || isKeyguardOccluded); updateStateForFlag(FLAG_KEYGUARD_OCCLUDED, isKeyguardOccluded); applyState(); } /** {@code true} if a slide in view is currently visible over taskbar. */ public void setSlideInViewVisible(boolean isSlideInViewVisible) { updateStateForFlag(FLAG_SLIDE_IN_VIEW_VISIBLE, isSlideInViewVisible); applyState(); } /** * Returns true if IME bar is visible */ public boolean isImeVisible() { return (mState & FLAG_IME_VISIBLE) != 0; } public boolean isImeRenderingNavButtons() { return mIsImeRenderingNavButtons; } /** * Returns true if the home button is disabled */ public boolean isHomeDisabled() { return (mState & FLAG_DISABLE_HOME) != 0; } /** * Returns true if the recents (overview) button is disabled */ public boolean isRecentsDisabled() { return (mState & FLAG_DISABLE_RECENTS) != 0; } /** * Adds the bounds corresponding to all visible buttons to provided region */ public void addVisibleButtonsRegion(BaseDragLayer parent, Region outRegion) { int count = mAllButtons.size(); for (int i = 0; i < count; i++) { View button = mAllButtons.get(i); if (button.getVisibility() == View.VISIBLE) { parent.getDescendantRectRelativeToSelf(button, mTempRect); if (mHitboxExtender.extendedHitboxEnabled()) { mTempRect.bottom += mContext.getDeviceProfile().getTaskbarOffsetY(); } outRegion.op(mTempRect, Op.UNION); } } } /** * Returns multi-value alpha controller for back button. */ public MultiValueAlpha getBackButtonAlpha() { return mBackButtonAlpha; } /** * Returns multi-value alpha controller for home button. */ public MultiValueAlpha getHomeButtonAlpha() { return mHomeButtonAlpha; } /** * Sets the AccessibilityDelegate for the home button. */ public void setHomeButtonAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) { if (mHomeButton == null) { return; } mHomeButton.setAccessibilityDelegate(accessibilityDelegate); } /** * Sets the AccessibilityDelegate for the back button. */ public void setBackButtonAccessibilityDelegate(AccessibilityDelegate accessibilityDelegate) { if (mBackButton == null) { return; } mBackButton.setAccessibilityDelegate(accessibilityDelegate); } public void setWallpaperVisible(boolean isVisible) { if (mContext.isPhoneMode()) { mTaskbarTransitions.setWallpaperVisibility(isVisible); } } public void onTransitionModeUpdated(int barMode, boolean checkBarModes) { mTransitionMode = barMode; if (checkBarModes) { checkNavBarModes(); } } public void checkNavBarModes() { if (mContext.isPhoneMode()) { boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0; mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden); } } public void finishBarAnimations() { if (mContext.isPhoneMode()) { mTaskbarTransitions.finishAnimations(); } } public void touchAutoDim(boolean reset) { if (mContext.isPhoneMode()) { mTaskbarTransitions.setAutoDim(false); mHandler.removeCallbacks(mAutoDim); if (reset) { mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS); } } } public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) { if (mContext.isPhoneMode()) { mTaskbarTransitions.transitionTo(barMode, animate); } } /** Use to set the translationY for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonTranslationY() { return mTaskbarNavButtonTranslationY; } /** Use to set the translationY for the all nav+contextual buttons when in Launcher */ public AnimatedFloat getTaskbarNavButtonTranslationYForInAppDisplay() { return mTaskbarNavButtonTranslationYForInAppDisplay; } /** Use to set the dark intensity for the all nav+contextual buttons */ public AnimatedFloat getTaskbarNavButtonDarkIntensity() { return mTaskbarNavButtonDarkIntensity; } /** Use to override the nav button color with {@link #mOnBackgroundIconColor}. */ public AnimatedFloat getOnTaskbarBackgroundNavButtonColorOverride() { return mOnTaskbarBackgroundNavButtonColorOverride; } /** * Does not call {@link #applyState()}. Don't forget to! */ private void updateStateForFlag(int flag, boolean enabled) { if (enabled) { mState |= flag; } else { mState &= ~flag; } } private void applyState() { int count = mPropertyHolders.size(); for (int i = 0; i < count; i++) { mPropertyHolders.get(i).setState(mState, mContext.isGestureNav()); } } private void updateNavButtonInAppDisplayProgressForSysui() { TaskbarUIController uiController = mControllers.uiController; if (uiController instanceof LauncherTaskbarUIController) { ((LauncherTaskbarUIController) uiController).onTaskbarInAppDisplayProgressUpdate( mNavButtonInAppDisplayProgressForSysui.value, SYSUI_SURFACE_PROGRESS_INDEX); } } /** * Sets the translationY of the nav buttons based on the current device state. */ public void updateNavButtonTranslationY() { if (mContext.isPhoneButtonNavMode()) { return; } final float normalTranslationY = mTaskbarNavButtonTranslationY.value; final float imeAdjustmentTranslationY = mTaskbarNavButtonTranslationYForIme.value; TaskbarUIController uiController = mControllers.uiController; final float inAppDisplayAdjustmentTranslationY = (uiController instanceof LauncherTaskbarUIController && ((LauncherTaskbarUIController) uiController).shouldUseInAppLayout()) ? mTaskbarNavButtonTranslationYForInAppDisplay.value : 0; mLastSetNavButtonTranslationY = normalTranslationY + imeAdjustmentTranslationY + inAppDisplayAdjustmentTranslationY; mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY); } /** * Sets Taskbar 3-button mode icon colors based on the * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases * in large screen taskbar where there may be opaque surfaces, the selected SystemUI button * colors are intentionally overridden. *

* This method is also called when any of the AnimatedFloat instances change. */ private void updateNavButtonColor() { final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance(); int taskbarNavButtonColor = getSysUiIconColorOnHome(argbEvaluator); // Only phone mode foldable button colors should be identical to SysUI navbar colors. if (!(ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode())) { taskbarNavButtonColor = getTaskbarButtonColor(argbEvaluator, taskbarNavButtonColor); } applyButtonColors(taskbarNavButtonColor); } /** * Taskbar 3-button mode icon colors based on the * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. */ private int getSysUiIconColorOnHome(ArgbEvaluator argbEvaluator) { return (int) argbEvaluator.evaluate(getTaskbarNavButtonDarkIntensity().value, mLightIconColorOnWorkspace, mDarkIconColorOnWorkspace); } /** * If Taskbar background is opaque or slide in overlay is visible, the selected SystemUI button * colors are intentionally overridden. The override can be disabled when * {@link #mOnBackgroundNavButtonColorOverrideMultiplier} is {@code 0}. */ private int getTaskbarButtonColor(ArgbEvaluator argbEvaluator, int sysUiIconColorOnHome) { final float sysUIColorOverride = mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max( mOnTaskbarBackgroundNavButtonColorOverride.value, mSlideInViewVisibleNavButtonColorOverride.value); return (int) argbEvaluator.evaluate(sysUIColorOverride, sysUiIconColorOnHome, mOnBackgroundIconColor); } /** * Iteratively sets button colors for each button in {@link #mAllButtons}. */ private void applyButtonColors(int iconColor) { for (ImageView button : mAllButtons) { button.setImageTintList(ColorStateList.valueOf(iconColor)); Drawable background = button.getBackground(); if (background instanceof KeyButtonRipple) { ((KeyButtonRipple) background).setDarkIntensity( getTaskbarNavButtonDarkIntensity().value); } } } /** * Updates Taskbar 3-Button icon colors as {@link #mTaskbarNavButtonDarkIntensity} changes. */ private void onDarkIntensityChanged() { updateNavButtonColor(); if (mContext.isPhoneMode()) { mTaskbarTransitions.onDarkIntensityChanged(getTaskbarNavButtonDarkIntensity().value); } } protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) { return addButton(drawableId, buttonType, parent, navButtonController, id, R.layout.taskbar_nav_button); } private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType, ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id, @LayoutRes int layoutId) { ImageView buttonView = addButton(parent, id, layoutId); buttonView.setImageResource(drawableId); buttonView.setContentDescription(parent.getContext().getString( navButtonController.getButtonContentDescription(buttonType))); if (predictiveBackThreeButtonNav() && buttonType == BUTTON_BACK) { // set up special touch listener for back button to support predictive back setBackButtonTouchListener(buttonView, navButtonController); } else { buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType, view)); buttonView.setOnLongClickListener(view -> navButtonController.onButtonLongClick(buttonType, view)); } return buttonView; } private void setBackButtonTouchListener(View buttonView, TaskbarNavButtonController navButtonController) { final RectF rect = new RectF(); buttonView.setOnTouchListener((v, event) -> { int motionEventAction = event.getAction(); if (motionEventAction == MotionEvent.ACTION_DOWN) { rect.set(0, 0, v.getWidth(), v.getHeight()); } boolean isCancelled = motionEventAction == MotionEvent.ACTION_CANCEL || (!rect.contains(event.getX(), event.getY()) && (motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP)); if (motionEventAction != MotionEvent.ACTION_DOWN && motionEventAction != MotionEvent.ACTION_UP && !isCancelled) { // return early. we don't care about any other cases than DOWN, UP and CANCEL return false; } int keyEventAction = motionEventAction == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : ACTION_UP; navButtonController.sendBackKeyEvent(keyEventAction, isCancelled); if (motionEventAction == MotionEvent.ACTION_UP && !isCancelled) { buttonView.performClick(); buttonView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } return false; }); buttonView.setOnLongClickListener((view) -> { navButtonController.onButtonLongClick(BUTTON_BACK, view); return false; }); } private ImageView addButton(ViewGroup parent, @IdRes int id, @LayoutRes int layoutId) { ImageView buttonView = (ImageView) mContext.getLayoutInflater() .inflate(layoutId, parent, false); buttonView.setId(id); parent.addView(buttonView); mAllButtons.add(buttonView); return buttonView; } public boolean isEventOverAnyItem(MotionEvent ev) { return mFloatingRotationButtonBounds.contains((int) ev.getX(), (int) ev.getY()); } public void onConfigurationChanged(@Config int configChanges) { if (mFloatingRotationButton != null) { mFloatingRotationButton.onConfigurationChanged(configChanges); } if (!mContext.isUserSetupComplete()) { handleSetupUi(); } updateButtonLayoutSpacing(); } private void handleSetupUi() { // Since setup wizard only has back button enabled, it looks strange to be // end-aligned, so start-align instead. FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); FrameLayout.LayoutParams navButtonsViewLayoutParams = (FrameLayout.LayoutParams) mNavButtonsView.getLayoutParams(); Resources resources = mContext.getResources(); DeviceProfile deviceProfile = mContext.getDeviceProfile(); navButtonsLayoutParams.setMarginEnd(0); navButtonsLayoutParams.gravity = Gravity.START; mControllers.taskbarActivityContext.setTaskbarWindowSize( mControllers.taskbarActivityContext.getSetupWindowSize()); // If SUW is on a large screen device that is landscape (or has a square aspect // ratio) the back button has to be placed accordingly if ((deviceProfile.isTablet && deviceProfile.isLandscape) || (deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND)) { navButtonsLayoutParams.setMarginStart( resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_start_margin)); navButtonsViewLayoutParams.bottomMargin = resources.getDimensionPixelSize( R.dimen.taskbar_back_button_suw_bottom_margin); navButtonsLayoutParams.height = resources.getDimensionPixelSize( R.dimen.taskbar_back_button_suw_height); } else { int phoneOrPortraitSetupMargin = resources.getDimensionPixelSize( R.dimen.taskbar_contextual_button_suw_margin); navButtonsLayoutParams.setMarginStart(phoneOrPortraitSetupMargin); navButtonsLayoutParams.bottomMargin = !deviceProfile.isLandscape ? 0 : phoneOrPortraitSetupMargin - (resources.getDimensionPixelSize( R.dimen.taskbar_nav_buttons_size) / 2); navButtonsViewLayoutParams.height = resources.getDimensionPixelSize( R.dimen.taskbar_contextual_button_suw_height); } mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams); mNavButtonContainer.setLayoutParams(navButtonsLayoutParams); } /** * Adds the correct spacing to 3 button nav container depending on if device is in kids mode, * setup wizard, or normal 3 button nav. */ private void updateButtonLayoutSpacing() { boolean isThreeButtonNav = mContext.isThreeButtonNav(); DeviceProfile dp = mContext.getDeviceProfile(); Resources res = mContext.getResources(); boolean isInSetup = !mContext.isUserSetupComplete(); // TODO(b/244231596) we're getting the incorrect kidsMode value in small-screen boolean isInKidsMode = mContext.isNavBarKidsModeActive(); if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { NavButtonLayoutter navButtonLayoutter = NavButtonLayoutFactory.Companion.getUiLayoutter( dp, mNavButtonsView, mImeSwitcherButton, mA11yButton, mSpace, res, isInKidsMode, isInSetup, isThreeButtonNav, mContext.isPhoneMode(), mWindowManagerProxy.getRotation(mContext)); navButtonLayoutter.layoutButtons(mContext, isA11yButtonPersistent()); updateButtonsBackground(); updateNavButtonColor(); return; } if (isInSetup) { handleSetupUi(); } else if (isInKidsMode) { int iconSize = res.getDimensionPixelSize( R.dimen.taskbar_icon_size_kids); int buttonWidth = res.getDimensionPixelSize( R.dimen.taskbar_nav_buttons_width_kids); int buttonHeight = res.getDimensionPixelSize( R.dimen.taskbar_nav_buttons_height_kids); int buttonRadius = res.getDimensionPixelSize( R.dimen.taskbar_nav_buttons_corner_radius_kids); int paddingleft = (buttonWidth - iconSize) / 2; int paddingRight = paddingleft; int paddingTop = (buttonHeight - iconSize) / 2; int paddingBottom = paddingTop; // Update icons final RotateDrawable rotateDrawable = new RotateDrawable(); rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back_kids)); rotateDrawable.setFromDegrees(0f); rotateDrawable.setToDegrees(-90f); mBackButton.setImageDrawable(rotateDrawable); mBackButton.setScaleType(ImageView.ScaleType.FIT_CENTER); mBackButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom); mHomeButton.setImageDrawable( mHomeButton.getContext().getDrawable(R.drawable.ic_sysbar_home_kids)); mHomeButton.setScaleType(ImageView.ScaleType.FIT_CENTER); mHomeButton.setPadding(paddingleft, paddingTop, paddingRight, paddingBottom); // Home button layout LinearLayout.LayoutParams homeLayoutparams = new LinearLayout.LayoutParams( buttonWidth, buttonHeight ); int homeButtonLeftMargin = res.getDimensionPixelSize( R.dimen.taskbar_home_button_left_margin_kids); homeLayoutparams.setMargins(homeButtonLeftMargin, 0, 0, 0); mHomeButton.setLayoutParams(homeLayoutparams); // Back button layout LinearLayout.LayoutParams backLayoutParams = new LinearLayout.LayoutParams( buttonWidth, buttonHeight ); int backButtonLeftMargin = res.getDimensionPixelSize( R.dimen.taskbar_back_button_left_margin_kids); backLayoutParams.setMargins(backButtonLeftMargin, 0, 0, 0); mBackButton.setLayoutParams(backLayoutParams); // Button backgrounds int whiteWith10PctAlpha = Color.argb(0.1f, 1, 1, 1); PaintDrawable buttonBackground = new PaintDrawable(whiteWith10PctAlpha); buttonBackground.setCornerRadius(buttonRadius); mHomeButton.setBackground(buttonBackground); mBackButton.setBackground(buttonBackground); // Update alignment within taskbar FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); navButtonsLayoutParams.setMarginStart( navButtonsLayoutParams.getMarginEnd() / 2); navButtonsLayoutParams.setMarginEnd(navButtonsLayoutParams.getMarginStart()); navButtonsLayoutParams.gravity = Gravity.CENTER; mNavButtonContainer.requestLayout(); mHomeButton.setOnLongClickListener(null); } else if (isThreeButtonNav) { final RotateDrawable rotateDrawable = new RotateDrawable(); rotateDrawable.setDrawable(mContext.getDrawable(R.drawable.ic_sysbar_back)); rotateDrawable.setFromDegrees(0f); rotateDrawable.setToDegrees(Utilities.isRtl(mContext.getResources()) ? 90f : -90f); mBackButton.setImageDrawable(rotateDrawable); // Setup normal 3 button // Add spacing after the end of the last nav button FrameLayout.LayoutParams navButtonParams = (FrameLayout.LayoutParams) mNavButtonContainer.getLayoutParams(); navButtonParams.gravity = Gravity.END; navButtonParams.width = FrameLayout.LayoutParams.WRAP_CONTENT; navButtonParams.height = MATCH_PARENT; int navMarginEnd = (int) res.getDimension(dp.inv.inlineNavButtonsEndSpacing); int contextualWidth = mEndContextualContainer.getWidth(); // If contextual buttons are showing, we check if the end margin is enough for the // contextual button to be showing - if not, move the nav buttons over a smidge if (isA11yButtonPersistent() && navMarginEnd < contextualWidth) { // Additional spacing, eat up half of space between last icon and nav button navMarginEnd += res.getDimensionPixelSize(R.dimen.taskbar_hotseat_nav_spacing) / 2; } navButtonParams.setMarginEnd(navMarginEnd); mNavButtonContainer.setLayoutParams(navButtonParams); // Add the spaces in between the nav buttons int spaceInBetween = res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween); for (int i = 0; i < mNavButtonContainer.getChildCount(); i++) { View navButton = mNavButtonContainer.getChildAt(i); LinearLayout.LayoutParams buttonLayoutParams = (LinearLayout.LayoutParams) navButton.getLayoutParams(); buttonLayoutParams.weight = 0; if (i == 0) { buttonLayoutParams.setMarginEnd(spaceInBetween / 2); } else if (i == mNavButtonContainer.getChildCount() - 1) { buttonLayoutParams.setMarginStart(spaceInBetween / 2); } else { buttonLayoutParams.setMarginStart(spaceInBetween / 2); buttonLayoutParams.setMarginEnd(spaceInBetween / 2); } } } } private void updateButtonsBackground() { boolean clipped = !mContext.isPhoneButtonNavMode(); mNavButtonContainer.setClipToPadding(clipped); mNavButtonContainer.setClipChildren(clipped); mNavButtonsView.setClipToPadding(clipped); mNavButtonsView.setClipChildren(clipped); for (ImageView button : mAllButtons) { updateButtonBackground(button, mContext.isPhoneButtonNavMode()); } } private static void updateButtonBackground(View view, boolean isPhoneButtonNavMode) { if (isPhoneButtonNavMode) { view.setBackground(new KeyButtonRipple(view.getContext(), view, R.dimen.key_button_ripple_max_width)); } else { view.setBackgroundResource(R.drawable.taskbar_icon_click_feedback_roundrect); } } public void onDestroy() { mPropertyHolders.clear(); mControllers.rotationButtonController.unregisterListeners(); if (mFloatingRotationButton != null) { mFloatingRotationButton.hide(); } moveNavButtonsBackToTaskbarWindow(); mNavButtonContainer.removeAllViews(); mEndContextualContainer.removeAllViews(); mStartContextualContainer.removeAllViews(); mAllButtons.clear(); } /** * Moves mNavButtonsView from TaskbarDragLayer to a placeholder BaseDragLayer on a new window. */ public void moveNavButtonsToNewWindow() { if (mAreNavButtonsInSeparateWindow) { return; } if (mIsImeRenderingNavButtons) { // IME is rendering the nav buttons, so we don't need to create a new layer for them. return; } mSeparateWindowParent.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { mSeparateWindowParent.getViewTreeObserver().addOnComputeInternalInsetsListener( mSeparateWindowInsetsComputer); } @Override public void onViewDetachedFromWindow(View view) { mSeparateWindowParent.removeOnAttachStateChangeListener(this); mSeparateWindowParent.getViewTreeObserver().removeOnComputeInternalInsetsListener( mSeparateWindowInsetsComputer); } }); mAreNavButtonsInSeparateWindow = true; mContext.getDragLayer().removeView(mNavButtonsView); mSeparateWindowParent.addView(mNavButtonsView); WindowManager.LayoutParams windowLayoutParams = mContext.createDefaultWindowLayoutParams( TYPE_NAVIGATION_BAR_PANEL, NAV_BUTTONS_SEPARATE_WINDOW_TITLE); mContext.addWindowView(mSeparateWindowParent, windowLayoutParams); } /** * Moves mNavButtonsView from its temporary window and reattaches it to TaskbarDragLayer. */ public void moveNavButtonsBackToTaskbarWindow() { if (!mAreNavButtonsInSeparateWindow) { return; } mAreNavButtonsInSeparateWindow = false; mContext.removeWindowView(mSeparateWindowParent); mSeparateWindowParent.removeView(mNavButtonsView); mContext.getDragLayer().addView(mNavButtonsView); } private void onComputeInsetsForSeparateWindow(ViewTreeObserver.InternalInsetsInfo insetsInfo) { addVisibleButtonsRegion(mSeparateWindowParent, insetsInfo.touchableRegion); insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION); } /** * Called whenever a new ui controller is set, and should update anything that depends on the * ui controller. */ public void onUiControllerChanged() { updateNavButtonInAppDisplayProgressForSysui(); updateNavButtonTranslationY(); } @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "NavbarButtonsViewController:"); pw.println(prefix + "\tmState=" + getStateString(mState)); pw.println(prefix + "\tmFloatingRotationButtonBounds=" + mFloatingRotationButtonBounds); pw.println(prefix + "\tmSysuiStateFlags=" + QuickStepContract.getSystemUiStateString( mSysuiStateFlags)); pw.println(prefix + "\tLast set nav button translationY=" + mLastSetNavButtonTranslationY); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationY=" + mTaskbarNavButtonTranslationY.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForInAppDisplay=" + mTaskbarNavButtonTranslationYForInAppDisplay.value); pw.println(prefix + "\t\tmTaskbarNavButtonTranslationYForIme=" + mTaskbarNavButtonTranslationYForIme.value); pw.println(prefix + "\t\tmTaskbarNavButtonDarkIntensity=" + mTaskbarNavButtonDarkIntensity.value); pw.println(prefix + "\t\tmSlideInViewVisibleNavButtonColorOverride=" + mSlideInViewVisibleNavButtonColorOverride.value); pw.println(prefix + "\t\tmOnTaskbarBackgroundNavButtonColorOverride=" + mOnTaskbarBackgroundNavButtonColorOverride.value); pw.println(prefix + "\t\tmOnBackgroundNavButtonColorOverrideMultiplier=" + mOnBackgroundNavButtonColorOverrideMultiplier.value); mNavButtonsView.dumpLogs(prefix + "\t", pw); if (mContext.isPhoneMode()) { mTaskbarTransitions.dumpLogs(prefix + "\t", pw); } } private static String getStateString(int flags) { StringJoiner str = new StringJoiner("|"); appendFlag(str, flags, FLAG_SWITCHER_SHOWING, "FLAG_SWITCHER_SHOWING"); appendFlag(str, flags, FLAG_IME_VISIBLE, "FLAG_IME_VISIBLE"); appendFlag(str, flags, FLAG_ROTATION_BUTTON_VISIBLE, "FLAG_ROTATION_BUTTON_VISIBLE"); appendFlag(str, flags, FLAG_A11Y_VISIBLE, "FLAG_A11Y_VISIBLE"); appendFlag(str, flags, FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, "FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE"); appendFlag(str, flags, FLAG_KEYGUARD_VISIBLE, "FLAG_KEYGUARD_VISIBLE"); appendFlag(str, flags, FLAG_KEYGUARD_OCCLUDED, "FLAG_KEYGUARD_OCCLUDED"); appendFlag(str, flags, FLAG_DISABLE_HOME, "FLAG_DISABLE_HOME"); appendFlag(str, flags, FLAG_DISABLE_RECENTS, "FLAG_DISABLE_RECENTS"); appendFlag(str, flags, FLAG_DISABLE_BACK, "FLAG_DISABLE_BACK"); appendFlag(str, flags, FLAG_NOTIFICATION_SHADE_EXPANDED, "FLAG_NOTIFICATION_SHADE_EXPANDED"); appendFlag(str, flags, FLAG_SCREEN_PINNING_ACTIVE, "FLAG_SCREEN_PINNING_ACTIVE"); appendFlag(str, flags, FLAG_VOICE_INTERACTION_WINDOW_SHOWING, "FLAG_VOICE_INTERACTION_WINDOW_SHOWING"); appendFlag(str, flags, FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING, "FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING"); return str.toString(); } public TouchController getTouchController() { return mHitboxExtender; } /** * @param alignment 0 -> Taskbar, 1 -> Workspace */ public void updateTaskbarAlignment(float alignment) { mHitboxExtender.onAnimationProgressToOverview(alignment); } /** Adjusts navigation buttons layout accordingly to the bubble bar position. */ @Override public void onBubbleBarLocationUpdated(BubbleBarLocation location) { boolean locationUpdated = location != mBubbleBarTargetLocation; if (locationUpdated) { cancelExistingNavBarAnimation(); } else { endExistingAnimation(); } mNavButtonContainer.setTranslationX(getNavBarTranslationX(location)); mBubbleBarTargetLocation = location; } /** Animates navigation buttons accordingly to the bubble bar position. */ @Override public void onBubbleBarLocationAnimated(BubbleBarLocation location) { if (location == mBubbleBarTargetLocation) return; cancelExistingNavBarAnimation(); mBubbleBarTargetLocation = location; int finalX = getNavBarTranslationX(location); Animator teleportAnimator = BarsLocationAnimatorHelper .getTeleportAnimatorForNavButtons(location, mNavButtonContainer, finalX); teleportAnimator.addListener(forEndCallback(() -> mNavBarLocationAnimator = null)); mNavBarLocationAnimator = teleportAnimator; mNavBarLocationAnimator.start(); } private void endExistingAnimation() { if (mNavBarLocationAnimator != null) { mNavBarLocationAnimator.end(); mNavBarLocationAnimator = null; } } private void cancelExistingNavBarAnimation() { if (mNavBarLocationAnimator != null) { mNavBarLocationAnimator.cancel(); mNavBarLocationAnimator = null; } } private int getNavBarTranslationX(BubbleBarLocation location) { boolean isNavbarOnRight = location.isOnLeft(mNavButtonsView.isLayoutRtl()); DeviceProfile dp = mContext.getDeviceProfile(); float navBarTargetStartX; if (!mContext.isUserSetupComplete()) { // Skip additional translations on the nav bar container while in SUW layout return 0; } else if (mContext.shouldStartAlignTaskbar()) { int navBarSpacing = dp.inlineNavButtonsEndSpacingPx; // If the taskbar is start aligned the navigation bar is aligned to the start or end of // the container, depending on the bubble bar location if (isNavbarOnRight) { navBarTargetStartX = dp.widthPx - navBarSpacing - mNavButtonContainer.getWidth(); } else { navBarTargetStartX = navBarSpacing; } } else { // If the task bar is not start aligned, the navigation bar is located in the center // between the taskbar and screen edges, depending on the bubble bar location. float navbarWidth = mNavButtonContainer.getWidth(); Rect taskbarBounds = mControllers.taskbarViewController .getTransientTaskbarIconLayoutBoundsInParent(); if (isNavbarOnRight) { if (mNavButtonsView.isLayoutRtl()) { float taskBarEnd = taskbarBounds.right; navBarTargetStartX = (dp.widthPx + taskBarEnd - navbarWidth) / 2; } else { navBarTargetStartX = mNavButtonContainer.getLeft(); } } else { float taskBarStart = taskbarBounds.left; navBarTargetStartX = (taskBarStart - navbarWidth) / 2; } } return (int) navBarTargetStartX - mNavButtonContainer.getLeft(); } /** Adjusts the navigation buttons layout position according to the bubble bar location. */ public void onLayoutsUpdated() { // no need to do anything if on phone, or if taskbar or navbar views were not placed on // screen. Rect transientTaskbarIconLayoutBoundsInParent = mControllers.taskbarViewController .getTransientTaskbarIconLayoutBoundsInParent(); if (mContext.getDeviceProfile().isPhone || transientTaskbarIconLayoutBoundsInParent.isEmpty() || mNavButtonsView.getWidth() == 0) { return; } if (enableBubbleBarInPersistentTaskBar() && mControllers.bubbleControllers.isPresent()) { if (mBubbleBarTargetLocation == null) { // only set bubble bar location if it was not set before mBubbleBarTargetLocation = mControllers.bubbleControllers.get() .bubbleBarViewController.getBubbleBarLocation(); } onBubbleBarLocationUpdated(mBubbleBarTargetLocation); } } private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback { @Override public void onVisibilityChanged(boolean isVisible) { if (isVisible) { mFloatingRotationButton.getCurrentView() .getBoundsOnScreen(mFloatingRotationButtonBounds); } else { mFloatingRotationButtonBounds.setEmpty(); } } } private static class StatePropertyHolder { private final float mEnabledValue, mDisabledValue; private final ObjectAnimator mAnimator; private final IntPredicate mEnableCondition; private boolean mIsEnabled = true; StatePropertyHolder(View view, IntPredicate enableCondition) { this(view, enableCondition, LauncherAnimUtils.VIEW_ALPHA, 1, 0); mAnimator.addListener(new AlphaUpdateListener(view)); } StatePropertyHolder(MultiProperty alphaProperty, IntPredicate enableCondition) { this(alphaProperty, enableCondition, MULTI_PROPERTY_VALUE, 1, 0); } StatePropertyHolder(AnimatedFloat animatedFloat, IntPredicate enableCondition) { this(animatedFloat, enableCondition, AnimatedFloat.VALUE, 1, 0); } StatePropertyHolder(T target, IntPredicate enabledCondition, Property property, float enabledValue, float disabledValue) { mEnableCondition = enabledCondition; mEnabledValue = enabledValue; mDisabledValue = disabledValue; mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue); } public void setState(int flags, boolean skipAnimation) { boolean isEnabled = mEnableCondition.test(flags); if (mIsEnabled != isEnabled) { mIsEnabled = isEnabled; mAnimator.cancel(); mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue); mAnimator.start(); if (skipAnimation) { mAnimator.end(); } } } public void endAnimation() { if (mAnimator.isRunning()) { mAnimator.end(); } } } }