/* * 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.HapticFeedbackConstants.LONG_PRESS; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.annotation.Nullable; import android.content.SharedPreferences; import android.content.res.Resources; import android.view.ViewConfiguration; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.quickstep.AnimatedFloat; import com.android.quickstep.SystemUiProxy; import java.util.function.IntPredicate; /** * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to * create a cohesive animation between stashed/unstashed states. */ public class TaskbarStashController { public static final int FLAG_IN_APP = 1 << 0; public static final int FLAG_STASHED_IN_APP_MANUAL = 1 << 1; // long press, persisted public static final int FLAG_STASHED_IN_APP_PINNED = 1 << 2; // app pinning public static final int FLAG_STASHED_IN_APP_EMPTY = 1 << 3; // no hotseat icons public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 4; // setup wizard and AllSetActivity public static final int FLAG_STASHED_IN_APP_IME = 1 << 5; // IME is visible public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 6; // If we're in an app and any of these flags are enabled, taskbar should be stashed. private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL | FLAG_STASHED_IN_APP_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP | FLAG_STASHED_IN_APP_IME; // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed // height. This way the reported insets are consistent even during transitions out of the app. // Currently any flag that causes us to stash in an app is included, except for IME since that // covers the underlying app anyway and thus the app shouldn't change insets. private static final int FLAGS_REPORT_STASHED_INSETS_TO_APP = FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME; /** * How long to stash/unstash when manually invoked via long press. */ public static final long TASKBAR_STASH_DURATION = 300; /** * How long to stash/unstash when keyboard is appearing/disappearing. */ private static final long TASKBAR_STASH_DURATION_FOR_IME = 80; /** * The scale TaskbarView animates to when being stashed. */ private static final float STASHED_TASKBAR_SCALE = 0.5f; /** * How long the hint animation plays, starting on motion down. */ private static final long TASKBAR_HINT_STASH_DURATION = ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT; /** * The scale that TaskbarView animates to when hinting towards the stashed state. */ private static final float STASHED_TASKBAR_HINT_SCALE = 0.9f; /** * The scale that the stashed handle animates to when hinting towards the unstashed state. */ private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f; /** * The SharedPreferences key for whether user has manually stashed the taskbar. */ private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed"; /** * Whether taskbar should be stashed out of the box. */ private static final boolean DEFAULT_STASHED_PREF = false; private final TaskbarActivityContext mActivity; private final SharedPreferences mPrefs; private final int mStashedHeight; private final int mUnstashedHeight; private final SystemUiProxy mSystemUiProxy; // Initialized in init. private TaskbarControllers mControllers; // Taskbar background properties. private AnimatedFloat mTaskbarBackgroundOffset; private AnimatedFloat mTaskbarImeBgAlpha; // TaskbarView icon properties. private AlphaProperty mIconAlphaForStash; private AnimatedFloat mIconScaleForStash; private AnimatedFloat mIconTranslationYForStash; // Stashed handle properties. private AlphaProperty mTaskbarStashedHandleAlpha; private AnimatedFloat mTaskbarStashedHandleHintScale; /** Whether we are currently visually stashed (might change based on launcher state). */ private boolean mIsStashed = false; private int mState; private @Nullable AnimatorSet mAnimator; private boolean mIsSystemGestureInProgress; private boolean mIsImeShowing; // Evaluate whether the handle should be stashed private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder( flags -> { boolean inApp = hasAnyFlag(flags, FLAG_IN_APP); boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP); boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE); return (inApp && stashedInApp) || (!inApp && stashedLauncherState); }); public TaskbarStashController(TaskbarActivityContext activity) { mActivity = activity; mPrefs = Utilities.getPrefs(mActivity); final Resources resources = mActivity.getResources(); mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize; } public void init(TaskbarControllers controllers, TaskbarSharedState sharedState) { mControllers = controllers; TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController; mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset(); mTaskbarImeBgAlpha = dragLayerController.getImeBgTaskbar(); TaskbarViewController taskbarViewController = controllers.taskbarViewController; mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty( TaskbarViewController.ALPHA_INDEX_STASH); mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash(); mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash(); StashedHandleViewController stashedHandleController = controllers.stashedHandleViewController; mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha().getProperty( StashedHandleViewController.ALPHA_INDEX_STASHED); mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale(); boolean isManuallyStashedInApp = supportsManualStashing() && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF); boolean isInSetup = !mActivity.isUserSetupComplete() || sharedState.setupUIVisible; updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp); // TODO(b/204384193): Temporarily disable SUW specific logic // updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup); if (isInSetup) { // Update the in-app state to ensure isStashed() reflects right state during SUW updateStateForFlag(FLAG_IN_APP, true); } applyState(); notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp()); } /** * Returns whether the taskbar can visually stash into a handle based on the current device * state. */ private boolean supportsVisualStashing() { return !mActivity.isThreeButtonNav(); } /** * Returns whether the user can manually stash the taskbar based on the current device state. */ private boolean supportsManualStashing() { return supportsVisualStashing() && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || supportsStashingForTests()); } private boolean supportsStashingForTests() { // TODO: enable this for tests that specifically check stash/unstash behavior. return false; } /** * Sets the flag indicating setup UI is visible */ protected void setSetupUIVisible(boolean isVisible) { updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isVisible || !mActivity.isUserSetupComplete()); applyState(); } /** * Returns whether the taskbar is currently visually stashed. */ public boolean isStashed() { return mIsStashed; } /** * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash). */ public boolean isStashedInApp() { return hasAnyFlag(FLAGS_STASHED_IN_APP); } /** * Returns whether the taskbar should be stashed in the current LauncherState. */ public boolean isInStashedLauncherState() { return hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing(); } private boolean hasAnyFlag(int flagMask) { return hasAnyFlag(mState, flagMask); } private boolean hasAnyFlag(int flags, int flagMask) { return (flags & flagMask) != 0; } /** * Returns whether the taskbar is currently visible and in an app. */ public boolean isInAppAndNotStashed() { return !mIsStashed && (mState & FLAG_IN_APP) != 0; } /** * Returns the height that taskbar will inset when inside apps. */ public int getContentHeightToReportToApps() { if (hasAnyFlag(FLAGS_REPORT_STASHED_INSETS_TO_APP)) { boolean isAnimating = mAnimator != null && mAnimator.isStarted(); return mControllers.stashedHandleViewController.isStashedHandleVisible() || isAnimating ? mStashedHeight : 0; } return mUnstashedHeight; } public int getStashedHeight() { return mStashedHeight; } /** * Should be called when long pressing the nav region when taskbar is present. * @return Whether taskbar was stashed and now is unstashed. */ public boolean onLongPressToUnstashTaskbar() { if (!isStashed()) { // We only listen for long press on the nav region to unstash the taskbar. To stash the // taskbar, we use an OnLongClickListener on TaskbarView instead. return false; } if (updateAndAnimateIsManuallyStashedInApp(false)) { mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS); return true; } return false; } /** * Updates whether we should stash the taskbar when in apps, and animates to the changed state. * @return Whether we started an animation to either be newly stashed or unstashed. */ public boolean updateAndAnimateIsManuallyStashedInApp(boolean isManuallyStashedInApp) { if (!supportsManualStashing()) { return false; } if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL) != isManuallyStashedInApp) { mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, isManuallyStashedInApp).apply(); updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp); applyState(); return true; } return false; } /** * Create a stash animation and save to {@link #mAnimator}. * @param isStashed whether it's a stash animation or an unstash animation * @param duration duration of the animation * @param startDelay how many milliseconds to delay the animation after starting it. */ private void createAnimToIsStashed(boolean isStashed, long duration, long startDelay) { if (mAnimator != null) { mAnimator.cancel(); } mAnimator = new AnimatorSet(); if (!supportsVisualStashing()) { // Just hide/show the icons and background instead of stashing into a handle. mAnimator.play(mIconAlphaForStash.animateToValue(isStashed ? 0 : 1) .setDuration(duration)); mAnimator.play(mTaskbarImeBgAlpha.animateToValue( hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration)); mAnimator.setStartDelay(startDelay); mAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mAnimator = null; } }); return; } AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); // Not exactly half and may overlap. See [first|second]HalfDurationScale below. AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); AnimatorSet secondHalfAnimatorSet = new AnimatorSet(); final float firstHalfDurationScale; final float secondHalfDurationScale; if (isStashed) { firstHalfDurationScale = 0.75f; secondHalfDurationScale = 0.5f; final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f; fullLengthAnimatorSet.playTogether( mTaskbarBackgroundOffset.animateToValue(1), mIconTranslationYForStash.animateToValue(stashTranslation) ); firstHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(0), mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE) ); secondHalfAnimatorSet.playTogether( mTaskbarStashedHandleAlpha.animateToValue(1) ); } else { firstHalfDurationScale = 0.5f; secondHalfDurationScale = 0.75f; fullLengthAnimatorSet.playTogether( mTaskbarBackgroundOffset.animateToValue(0), mIconScaleForStash.animateToValue(1), mIconTranslationYForStash.animateToValue(0) ); firstHalfAnimatorSet.playTogether( mTaskbarStashedHandleAlpha.animateToValue(0) ); secondHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(1) ); } fullLengthAnimatorSet.play(mControllers.stashedHandleViewController .createRevealAnimToIsStashed(isStashed)); // Return the stashed handle to its default scale in case it was changed as part of the // feedforward hint. Note that the reveal animation above also visually scales it. fullLengthAnimatorSet.play(mTaskbarStashedHandleHintScale.animateToValue(1f)); fullLengthAnimatorSet.setDuration(duration); firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, secondHalfAnimatorSet); mAnimator.setStartDelay(startDelay); mAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mIsStashed = isStashed; onIsStashed(mIsStashed); } @Override public void onAnimationEnd(Animator animation) { mAnimator = null; } }); } /** * Creates and starts a partial stash animation, hinting at the new state that will trigger when * long press is detected. * @param animateForward Whether we are going towards the new stashed state or returning to the * unstashed state. */ public void startStashHint(boolean animateForward) { if (isStashed() || !supportsManualStashing()) { // Already stashed, no need to hint in that direction. return; } mIconScaleForStash.animateToValue( animateForward ? STASHED_TASKBAR_HINT_SCALE : 1) .setDuration(TASKBAR_HINT_STASH_DURATION).start(); } /** * Creates and starts a partial unstash animation, hinting at the new state that will trigger * when long press is detected. * @param animateForward Whether we are going towards the new unstashed state or returning to * the stashed state. */ public void startUnstashHint(boolean animateForward) { if (!isStashed()) { // Already unstashed, no need to hint in that direction. return; } mTaskbarStashedHandleHintScale.animateToValue( animateForward ? UNSTASHED_TASKBAR_HANDLE_HINT_SCALE : 1) .setDuration(TASKBAR_HINT_STASH_DURATION).start(); } private void onIsStashed(boolean isStashed) { mControllers.stashedHandleViewController.onIsStashed(isStashed); } public void applyState() { applyState(TASKBAR_STASH_DURATION); } public void applyState(long duration) { mStatePropertyHolder.setState(mState, duration, true); } public void applyState(long duration, long startDelay) { mStatePropertyHolder.setState(mState, duration, startDelay, true); } public Animator applyStateWithoutStart() { return applyStateWithoutStart(TASKBAR_STASH_DURATION); } public Animator applyStateWithoutStart(long duration) { return mStatePropertyHolder.setState(mState, duration, false); } /** * Should be called when a system gesture starts and settles, so we can defer updating * FLAG_STASHED_IN_APP_IME until after the gesture transition completes. */ public void setSystemGestureInProgress(boolean inProgress) { mIsSystemGestureInProgress = inProgress; // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. if (!mIsSystemGestureInProgress) { updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); applyState(TASKBAR_STASH_DURATION_FOR_IME, getTaskbarStashStartDelayForIme()); } } /** * When hiding the IME, delay the unstash animation to align with the end of the transition. */ private long getTaskbarStashStartDelayForIme() { if (mIsImeShowing) { // Only delay when IME is exiting, not entering. return 0; } // This duration is based on input_method_extract_exit.xml. long imeExitDuration = mControllers.taskbarActivityContext.getResources() .getInteger(android.R.integer.config_shortAnimTime); return imeExitDuration - TASKBAR_STASH_DURATION_FOR_IME; } /** Called when some system ui state has changed. (See SYSUI_STATE_... in QuickstepContract) */ public void updateStateForSysuiFlags(int systemUiStateFlags, boolean skipAnim) { long animDuration = TASKBAR_STASH_DURATION; long startDelay = 0; updateStateForFlag(FLAG_STASHED_IN_APP_PINNED, hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING)); // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING); if (!mIsSystemGestureInProgress) { updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); animDuration = TASKBAR_STASH_DURATION_FOR_IME; startDelay = getTaskbarStashStartDelayForIme(); } applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay); } /** * Updates the proper flag to indicate whether the task bar should be stashed. * * Note that this only updates the flag. {@link #applyState()} needs to be called separately. * * @param flag The flag to update. * @param enabled Whether to enable the flag: True will cause the task bar to be stashed / * unstashed. */ public void updateStateForFlag(int flag, boolean enabled) { if (enabled) { mState |= flag; } else { mState &= ~flag; } } /** * Called after updateStateForFlag() and applyState() have been called. * @param changedFlags The flags that have changed. */ private void onStateChangeApplied(int changedFlags) { if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP)) { mControllers.uiController.onStashedInAppChanged(); } if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP | FLAG_IN_APP)) { notifyStashChange(/* visible */ hasAnyFlag(FLAG_IN_APP), /* stashed */ isStashedInApp()); } if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_MANUAL)) { if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL)) { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_HIDE); } else { mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_SHOW); } } } private void notifyStashChange(boolean visible, boolean stashed) { mSystemUiProxy.notifyTaskbarStatus(visible, stashed); mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed); } private class StatePropertyHolder { private final IntPredicate mStashCondition; private boolean mIsStashed; private int mPrevFlags; StatePropertyHolder(IntPredicate stashCondition) { mStashCondition = stashCondition; } /** * @see #setState(int, long, long, boolean) with a default startDelay = 0. */ public Animator setState(int flags, long duration, boolean start) { return setState(flags, duration, 0 /* startDelay */, start); } /** * Applies the latest state, potentially calling onStateChangeApplied() and creating a new * animation (stored in mAnimator) which is started if {@param start} is true. * @param flags The latest flags to apply (see the top of this file). * @param duration The length of the animation. * @param startDelay How long to delay the animation after calling start(). * @param start Whether to start mAnimator immediately. * @return mAnimator if mIsStashed changed, else null. */ public Animator setState(int flags, long duration, long startDelay, boolean start) { int changedFlags = mPrevFlags ^ flags; if (mPrevFlags != flags) { onStateChangeApplied(changedFlags); mPrevFlags = flags; } boolean isStashed = mStashCondition.test(flags); if (mIsStashed != isStashed) { mIsStashed = isStashed; // This sets mAnimator. createAnimToIsStashed(mIsStashed, duration, startDelay); if (start) { mAnimator.start(); } return mAnimator; } return null; } } }