/* * Copyright (C) 2018 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.allapps; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.HOTSEAT; import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.PREDICTION; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.content.SharedPreferences; import android.os.Handler; import android.view.MotionEvent; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager; import com.android.launcher3.LauncherStateManager.StateListener; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.states.InternalStateHandler; /** * Abstract base class of floating view responsible for showing discovery bounce animation */ public class DiscoveryBounce extends AbstractFloatingView { private static final long DELAY_MS = 450; public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown"; public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen"; public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count"; public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count"; public static final int BOUNCE_MAX_COUNT = 3; private final Launcher mLauncher; private final Animator mDiscoBounceAnimation; private final StateListener mStateListener = new StateListener() { @Override public void onStateTransitionStart(LauncherState toState) { handleClose(false); } @Override public void onStateTransitionComplete(LauncherState finalState) {} }; public DiscoveryBounce(Launcher launcher, float delta) { super(launcher, null); mLauncher = launcher; AllAppsTransitionController controller = mLauncher.getAllAppsController(); mDiscoBounceAnimation = AnimatorInflater.loadAnimator(launcher, R.animator.discovery_bounce); mDiscoBounceAnimation.setTarget(new VerticalProgressWrapper(controller, delta)); mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { handleClose(false); } }); mDiscoBounceAnimation.addListener(controller.getProgressAnimatorListener()); launcher.getStateManager().addStateListener(mStateListener); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mDiscoBounceAnimation.start(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mDiscoBounceAnimation.isRunning()) { mDiscoBounceAnimation.end(); } } @Override public boolean onBackPressed() { super.onBackPressed(); // Go back to the previous state (from a user's perspective this floating view isn't // something to go back from). return false; } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { handleClose(false); return false; } @Override protected void handleClose(boolean animate) { if (mIsOpen) { mIsOpen = false; mLauncher.getDragLayer().removeView(this); // Reset the all-apps progress to what ever it was previously. mLauncher.getAllAppsController().setProgress(mLauncher.getStateManager() .getState().getVerticalProgress(mLauncher)); mLauncher.getStateManager().removeStateListener(mStateListener); } } @Override public void logActionCommand(int command) { // Since this is on-boarding popup, it is not a user controlled action. } @Override protected boolean isOfType(int type) { return (type & TYPE_DISCOVERY_BOUNCE) != 0; } private void show(int containerType) { mIsOpen = true; mLauncher.getDragLayer().addView(this); mLauncher.getUserEventDispatcher().logActionBounceTip(containerType); } public static void showForHomeIfNeeded(Launcher launcher) { showForHomeIfNeeded(launcher, true); } private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) { if (!launcher.isInState(NORMAL) || (launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false) && !shouldShowForWorkProfile(launcher)) || AbstractFloatingView.getTopOpenView(launcher) != null || UserManagerCompat.getInstance(launcher).isDemoUser() || Utilities.IS_RUNNING_IN_TEST_HARNESS) { return; } if (withDelay) { new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS); return; } incrementHomeBounceCount(launcher); new DiscoveryBounce(launcher, 0).show(HOTSEAT); } public static void showForOverviewIfNeeded(Launcher launcher) { showForOverviewIfNeeded(launcher, true); } private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay) { if (!launcher.isInState(OVERVIEW) || !launcher.hasBeenResumed() || launcher.isForceInvisible() || launcher.getDeviceProfile().isVerticalBarLayout() || (launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false) && !shouldShowForWorkProfile(launcher)) || UserManagerCompat.getInstance(launcher).isDemoUser() || Utilities.IS_RUNNING_IN_TEST_HARNESS) { return; } if (withDelay) { new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false), DELAY_MS); return; } else if (InternalStateHandler.hasPending() || AbstractFloatingView.getTopOpenView(launcher) != null) { // TODO: Move these checks to the top and call this method after invalidate handler. return; } incrementShelfBounceCount(launcher); new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher))) .show(PREDICTION); } /** * A wrapper around {@link AllAppsTransitionController} allowing a fixed shift in the value. */ public static class VerticalProgressWrapper { private final float mDelta; private final AllAppsTransitionController mController; private VerticalProgressWrapper(AllAppsTransitionController controller, float delta) { mController = controller; mDelta = delta; } public float getProgress() { return mController.getProgress() + mDelta; } public void setProgress(float progress) { mController.setProgress(progress - mDelta); } } private static boolean shouldShowForWorkProfile(Launcher launcher) { return !launcher.getSharedPrefs().getBoolean( PersonalWorkSlidingTabStrip.KEY_SHOWED_PEEK_WORK_TAB, false) && UserManagerCompat.getInstance(launcher).hasWorkProfile(); } private static void incrementShelfBounceCount(Launcher launcher) { SharedPreferences sharedPrefs = launcher.getSharedPrefs(); int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0); if (count > BOUNCE_MAX_COUNT) { return; } sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply(); } private static void incrementHomeBounceCount(Launcher launcher) { SharedPreferences sharedPrefs = launcher.getSharedPrefs(); int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0); if (count > BOUNCE_MAX_COUNT) { return; } sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply(); } }