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