1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.allapps; 18 19 import static com.android.launcher3.LauncherState.NORMAL; 20 import static com.android.launcher3.LauncherState.OVERVIEW; 21 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.HOTSEAT; 22 import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType.PREDICTION; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorInflater; 26 import android.animation.AnimatorListenerAdapter; 27 import android.content.SharedPreferences; 28 import android.os.Handler; 29 import android.view.MotionEvent; 30 31 import com.android.launcher3.AbstractFloatingView; 32 import com.android.launcher3.Launcher; 33 import com.android.launcher3.LauncherState; 34 import com.android.launcher3.LauncherStateManager; 35 import com.android.launcher3.LauncherStateManager.StateListener; 36 import com.android.launcher3.R; 37 import com.android.launcher3.Utilities; 38 import com.android.launcher3.compat.UserManagerCompat; 39 import com.android.launcher3.states.InternalStateHandler; 40 41 /** 42 * Abstract base class of floating view responsible for showing discovery bounce animation 43 */ 44 public class DiscoveryBounce extends AbstractFloatingView { 45 46 private static final long DELAY_MS = 450; 47 48 public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown"; 49 public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen"; 50 public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count"; 51 public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count"; 52 53 public static final int BOUNCE_MAX_COUNT = 3; 54 55 private final Launcher mLauncher; 56 private final Animator mDiscoBounceAnimation; 57 58 private final StateListener mStateListener = new StateListener() { 59 @Override 60 public void onStateTransitionStart(LauncherState toState) { 61 handleClose(false); 62 } 63 64 @Override 65 public void onStateTransitionComplete(LauncherState finalState) {} 66 }; 67 DiscoveryBounce(Launcher launcher, float delta)68 public DiscoveryBounce(Launcher launcher, float delta) { 69 super(launcher, null); 70 mLauncher = launcher; 71 AllAppsTransitionController controller = mLauncher.getAllAppsController(); 72 73 mDiscoBounceAnimation = 74 AnimatorInflater.loadAnimator(launcher, R.animator.discovery_bounce); 75 mDiscoBounceAnimation.setTarget(new VerticalProgressWrapper(controller, delta)); 76 mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() { 77 @Override 78 public void onAnimationEnd(Animator animation) { 79 handleClose(false); 80 } 81 }); 82 mDiscoBounceAnimation.addListener(controller.getProgressAnimatorListener()); 83 launcher.getStateManager().addStateListener(mStateListener); 84 } 85 86 @Override onAttachedToWindow()87 protected void onAttachedToWindow() { 88 super.onAttachedToWindow(); 89 mDiscoBounceAnimation.start(); 90 } 91 92 @Override onDetachedFromWindow()93 protected void onDetachedFromWindow() { 94 super.onDetachedFromWindow(); 95 if (mDiscoBounceAnimation.isRunning()) { 96 mDiscoBounceAnimation.end(); 97 } 98 } 99 100 @Override onBackPressed()101 public boolean onBackPressed() { 102 super.onBackPressed(); 103 // Go back to the previous state (from a user's perspective this floating view isn't 104 // something to go back from). 105 return false; 106 } 107 108 @Override onControllerInterceptTouchEvent(MotionEvent ev)109 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 110 handleClose(false); 111 return false; 112 } 113 114 @Override handleClose(boolean animate)115 protected void handleClose(boolean animate) { 116 if (mIsOpen) { 117 mIsOpen = false; 118 mLauncher.getDragLayer().removeView(this); 119 // Reset the all-apps progress to what ever it was previously. 120 mLauncher.getAllAppsController().setProgress(mLauncher.getStateManager() 121 .getState().getVerticalProgress(mLauncher)); 122 mLauncher.getStateManager().removeStateListener(mStateListener); 123 } 124 } 125 126 @Override logActionCommand(int command)127 public void logActionCommand(int command) { 128 // Since this is on-boarding popup, it is not a user controlled action. 129 } 130 131 @Override isOfType(int type)132 protected boolean isOfType(int type) { 133 return (type & TYPE_DISCOVERY_BOUNCE) != 0; 134 } 135 show(int containerType)136 private void show(int containerType) { 137 mIsOpen = true; 138 mLauncher.getDragLayer().addView(this); 139 mLauncher.getUserEventDispatcher().logActionBounceTip(containerType); 140 } 141 showForHomeIfNeeded(Launcher launcher)142 public static void showForHomeIfNeeded(Launcher launcher) { 143 showForHomeIfNeeded(launcher, true); 144 } 145 showForHomeIfNeeded(Launcher launcher, boolean withDelay)146 private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) { 147 if (!launcher.isInState(NORMAL) 148 || (launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false) 149 && !shouldShowForWorkProfile(launcher)) 150 || AbstractFloatingView.getTopOpenView(launcher) != null 151 || UserManagerCompat.getInstance(launcher).isDemoUser() 152 || Utilities.IS_RUNNING_IN_TEST_HARNESS) { 153 return; 154 } 155 156 if (withDelay) { 157 new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS); 158 return; 159 } 160 incrementHomeBounceCount(launcher); 161 162 new DiscoveryBounce(launcher, 0).show(HOTSEAT); 163 } 164 showForOverviewIfNeeded(Launcher launcher)165 public static void showForOverviewIfNeeded(Launcher launcher) { 166 showForOverviewIfNeeded(launcher, true); 167 } 168 showForOverviewIfNeeded(Launcher launcher, boolean withDelay)169 private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay) { 170 if (!launcher.isInState(OVERVIEW) 171 || !launcher.hasBeenResumed() 172 || launcher.isForceInvisible() 173 || launcher.getDeviceProfile().isVerticalBarLayout() 174 || (launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false) 175 && !shouldShowForWorkProfile(launcher)) 176 || UserManagerCompat.getInstance(launcher).isDemoUser() 177 || Utilities.IS_RUNNING_IN_TEST_HARNESS) { 178 return; 179 } 180 181 if (withDelay) { 182 new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false), DELAY_MS); 183 return; 184 } else if (InternalStateHandler.hasPending() 185 || AbstractFloatingView.getTopOpenView(launcher) != null) { 186 // TODO: Move these checks to the top and call this method after invalidate handler. 187 return; 188 } 189 incrementShelfBounceCount(launcher); 190 191 new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher))) 192 .show(PREDICTION); 193 } 194 195 /** 196 * A wrapper around {@link AllAppsTransitionController} allowing a fixed shift in the value. 197 */ 198 public static class VerticalProgressWrapper { 199 200 private final float mDelta; 201 private final AllAppsTransitionController mController; 202 VerticalProgressWrapper(AllAppsTransitionController controller, float delta)203 private VerticalProgressWrapper(AllAppsTransitionController controller, float delta) { 204 mController = controller; 205 mDelta = delta; 206 } 207 getProgress()208 public float getProgress() { 209 return mController.getProgress() + mDelta; 210 } 211 setProgress(float progress)212 public void setProgress(float progress) { 213 mController.setProgress(progress - mDelta); 214 } 215 } 216 shouldShowForWorkProfile(Launcher launcher)217 private static boolean shouldShowForWorkProfile(Launcher launcher) { 218 return !launcher.getSharedPrefs().getBoolean( 219 PersonalWorkSlidingTabStrip.KEY_SHOWED_PEEK_WORK_TAB, false) 220 && UserManagerCompat.getInstance(launcher).hasWorkProfile(); 221 } 222 incrementShelfBounceCount(Launcher launcher)223 private static void incrementShelfBounceCount(Launcher launcher) { 224 SharedPreferences sharedPrefs = launcher.getSharedPrefs(); 225 int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0); 226 if (count > BOUNCE_MAX_COUNT) { 227 return; 228 } 229 sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply(); 230 } 231 incrementHomeBounceCount(Launcher launcher)232 private static void incrementHomeBounceCount(Launcher launcher) { 233 SharedPreferences sharedPrefs = launcher.getSharedPrefs(); 234 int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0); 235 if (count > BOUNCE_MAX_COUNT) { 236 return; 237 } 238 sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply(); 239 } 240 } 241