1 /*
2  * Copyright (C) 2015 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.statemanager;
18 
19 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
20 
21 import android.animation.Animator;
22 import android.animation.Animator.AnimatorListener;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.util.Log;
28 
29 import com.android.launcher3.Utilities;
30 import com.android.launcher3.anim.AnimationSuccessListener;
31 import com.android.launcher3.anim.AnimatorPlaybackController;
32 import com.android.launcher3.anim.PendingAnimation;
33 import com.android.launcher3.states.StateAnimationConfig;
34 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
35 import com.android.launcher3.testing.TestProtocol;
36 
37 import java.io.PrintWriter;
38 import java.util.ArrayList;
39 
40 /**
41  * Class to manage transitions between different states for a StatefulActivity based on different
42  * states
43  */
44 public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
45 
46     public static final String TAG = "StateManager";
47 
48     private final AnimationState mConfig = new AnimationState();
49     private final Handler mUiHandler;
50     private final StatefulActivity<STATE_TYPE> mActivity;
51     private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
52     private final STATE_TYPE mBaseState;
53 
54     // Animators which are run on properties also controlled by state animations.
55     private final AtomicAnimationFactory mAtomicAnimationFactory;
56 
57     private StateHandler<STATE_TYPE>[] mStateHandlers;
58     private STATE_TYPE mState;
59 
60     private STATE_TYPE mLastStableState;
61     private STATE_TYPE mCurrentStableState;
62 
63     private STATE_TYPE mRestState;
64 
StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState)65     public StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState) {
66         mUiHandler = new Handler(Looper.getMainLooper());
67         mActivity = l;
68         mBaseState = baseState;
69         mState = mLastStableState = mCurrentStableState = baseState;
70         mAtomicAnimationFactory = l.createAtomicAnimationFactory();
71     }
72 
getState()73     public STATE_TYPE getState() {
74         return mState;
75     }
76 
getCurrentStableState()77     public STATE_TYPE getCurrentStableState() {
78         return mCurrentStableState;
79     }
80 
dump(String prefix, PrintWriter writer)81     public void dump(String prefix, PrintWriter writer) {
82         writer.println(prefix + "StateManager:");
83         writer.println(prefix + "\tmLastStableState:" + mLastStableState);
84         writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
85         writer.println(prefix + "\tmState:" + mState);
86         writer.println(prefix + "\tmRestState:" + mRestState);
87         writer.println(prefix + "\tisInTransition:" + (mConfig.currentAnimation != null));
88     }
89 
getStateHandlers()90     public StateHandler[] getStateHandlers() {
91         if (mStateHandlers == null) {
92             mStateHandlers = mActivity.createStateHandlers();
93         }
94         return mStateHandlers;
95     }
96 
addStateListener(StateListener listener)97     public void addStateListener(StateListener listener) {
98         mListeners.add(listener);
99     }
100 
removeStateListener(StateListener listener)101     public void removeStateListener(StateListener listener) {
102         mListeners.remove(listener);
103     }
104 
105     /**
106      * Returns true if the state changes should be animated.
107      */
shouldAnimateStateChange()108     public boolean shouldAnimateStateChange() {
109         return !mActivity.isForceInvisible() && mActivity.isStarted();
110     }
111 
112     /**
113      * @return {@code true} if the state matches the current state and there is no active
114      *         transition to different state.
115      */
isInStableState(STATE_TYPE state)116     public boolean isInStableState(STATE_TYPE state) {
117         return mState == state && mCurrentStableState == state
118                 && (mConfig.targetState == null || mConfig.targetState == state);
119     }
120 
121     /**
122      * @see #goToState(STATE_TYPE, boolean, Runnable)
123      */
goToState(STATE_TYPE state)124     public void goToState(STATE_TYPE state) {
125         goToState(state, shouldAnimateStateChange());
126     }
127 
128     /**
129      * @see #goToState(STATE_TYPE, boolean, Runnable)
130      */
goToState(STATE_TYPE state, boolean animated)131     public void goToState(STATE_TYPE state, boolean animated) {
132         goToState(state, animated, 0, null);
133     }
134 
135     /**
136      * Changes the Launcher state to the provided state.
137      *
138      * @param animated false if the state should change immediately without any animation,
139      *                true otherwise
140      * @paras onCompleteRunnable any action to perform at the end of the transition, of null.
141      */
goToState(STATE_TYPE state, boolean animated, Runnable onCompleteRunnable)142     public void goToState(STATE_TYPE state, boolean animated, Runnable onCompleteRunnable) {
143         goToState(state, animated, 0, onCompleteRunnable);
144     }
145 
146     /**
147      * Changes the Launcher state to the provided state after the given delay.
148      */
goToState(STATE_TYPE state, long delay, Runnable onCompleteRunnable)149     public void goToState(STATE_TYPE state, long delay, Runnable onCompleteRunnable) {
150         goToState(state, true, delay, onCompleteRunnable);
151     }
152 
153     /**
154      * Changes the Launcher state to the provided state after the given delay.
155      */
goToState(STATE_TYPE state, long delay)156     public void goToState(STATE_TYPE state, long delay) {
157         goToState(state, true, delay, null);
158     }
159 
reapplyState()160     public void reapplyState() {
161         reapplyState(false);
162     }
163 
reapplyState(boolean cancelCurrentAnimation)164     public void reapplyState(boolean cancelCurrentAnimation) {
165         boolean wasInAnimation = mConfig.currentAnimation != null;
166         if (cancelCurrentAnimation) {
167             mAtomicAnimationFactory.cancelAllStateElementAnimation();
168             cancelAnimation();
169         }
170         if (mConfig.currentAnimation == null) {
171             for (StateHandler handler : getStateHandlers()) {
172                 handler.setState(mState);
173             }
174             if (wasInAnimation) {
175                 onStateTransitionEnd(mState);
176             }
177         }
178     }
179 
goToState(STATE_TYPE state, boolean animated, long delay, final Runnable onCompleteRunnable)180     private void goToState(STATE_TYPE state, boolean animated, long delay,
181             final Runnable onCompleteRunnable) {
182         animated &= Utilities.areAnimationsEnabled(mActivity);
183         if (mActivity.isInState(state)) {
184             if (mConfig.currentAnimation == null) {
185                 // Run any queued runnable
186                 if (onCompleteRunnable != null) {
187                     onCompleteRunnable.run();
188                 }
189                 return;
190             } else if (!mConfig.userControlled && animated && mConfig.targetState == state) {
191                 // We are running the same animation as requested
192                 if (onCompleteRunnable != null) {
193                     mConfig.currentAnimation.addListener(
194                             AnimationSuccessListener.forRunnable(onCompleteRunnable));
195                 }
196                 return;
197             }
198         }
199 
200         // Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
201         STATE_TYPE fromState = mState;
202         mConfig.reset();
203 
204         if (!animated) {
205             mAtomicAnimationFactory.cancelAllStateElementAnimation();
206             onStateTransitionStart(state);
207             for (StateHandler handler : getStateHandlers()) {
208                 handler.setState(state);
209             }
210 
211             onStateTransitionEnd(state);
212 
213             // Run any queued runnable
214             if (onCompleteRunnable != null) {
215                 onCompleteRunnable.run();
216             }
217             return;
218         }
219 
220         if (delay > 0) {
221             // Create the animation after the delay as some properties can change between preparing
222             // the animation and running the animation.
223             int startChangeId = mConfig.changeId;
224             mUiHandler.postDelayed(() -> {
225                 if (mConfig.changeId == startChangeId) {
226                     goToStateAnimated(state, fromState, onCompleteRunnable);
227                 }
228             }, delay);
229         } else {
230             goToStateAnimated(state, fromState, onCompleteRunnable);
231         }
232     }
233 
goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState, Runnable onCompleteRunnable)234     private void goToStateAnimated(STATE_TYPE state, STATE_TYPE fromState,
235             Runnable onCompleteRunnable) {
236         // Since state mBaseState can be reached from multiple states, just assume that the
237         // transition plays in reverse and use the same duration as previous state.
238         mConfig.duration = state == mBaseState
239                 ? fromState.getTransitionDuration(mActivity)
240                 : state.getTransitionDuration(mActivity);
241         prepareForAtomicAnimation(fromState, state, mConfig);
242         AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
243         if (onCompleteRunnable != null) {
244             animation.addListener(AnimationSuccessListener.forRunnable(onCompleteRunnable));
245         }
246         mUiHandler.post(new StartAnimRunnable(animation));
247     }
248 
249     /**
250      * Prepares for a non-user controlled animation from fromState to toState. Preparations include:
251      * - Setting interpolators for various animations included in the state transition.
252      * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
253      */
prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)254     public void prepareForAtomicAnimation(STATE_TYPE fromState, STATE_TYPE toState,
255             StateAnimationConfig config) {
256         mAtomicAnimationFactory.prepareForAtomicAnimation(fromState, toState, config);
257     }
258 
259     /**
260      * Creates an animation representing atomic transitions between the provided states
261      */
createAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)262     public AnimatorSet createAtomicAnimation(
263             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
264         PendingAnimation builder = new PendingAnimation(config.duration);
265         prepareForAtomicAnimation(fromState, toState, config);
266 
267         for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
268             handler.setStateWithAnimation(toState, config, builder);
269         }
270         return builder.buildAnim();
271     }
272 
273     /**
274      * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
275      * state transition.
276      * @param state the final state for the transition.
277      * @param duration intended duration for state playback. Use higher duration for better
278      *                accuracy.
279      */
createAnimationToNewWorkspace( STATE_TYPE state, long duration)280     public AnimatorPlaybackController createAnimationToNewWorkspace(
281             STATE_TYPE state, long duration) {
282         return createAnimationToNewWorkspace(state, duration, ANIM_ALL_COMPONENTS);
283     }
284 
createAnimationToNewWorkspace( STATE_TYPE state, long duration, @AnimationFlags int animComponents)285     public AnimatorPlaybackController createAnimationToNewWorkspace(
286             STATE_TYPE state, long duration, @AnimationFlags int animComponents) {
287         StateAnimationConfig config = new StateAnimationConfig();
288         config.duration = duration;
289         config.animFlags = animComponents;
290         return createAnimationToNewWorkspace(state, config);
291     }
292 
createAnimationToNewWorkspace(STATE_TYPE state, StateAnimationConfig config)293     public AnimatorPlaybackController createAnimationToNewWorkspace(STATE_TYPE state,
294             StateAnimationConfig config) {
295         config.userControlled = true;
296         mConfig.reset();
297         config.copyTo(mConfig);
298         mConfig.playbackController = createAnimationToNewWorkspaceInternal(state)
299                 .createPlaybackController();
300         return mConfig.playbackController;
301     }
302 
createAnimationToNewWorkspaceInternal(final STATE_TYPE state)303     private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
304         if (TestProtocol.sDebugTracing) {
305             Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "createAnimationToNewWorkspaceInternal: "
306                     + state);
307         }
308         PendingAnimation builder = new PendingAnimation(mConfig.duration);
309         if (mConfig.getAnimComponents() != 0) {
310             for (StateHandler handler : getStateHandlers()) {
311                 handler.setStateWithAnimation(state, mConfig, builder);
312             }
313         }
314         builder.addListener(new AnimationSuccessListener() {
315 
316             @Override
317             public void onAnimationStart(Animator animation) {
318                 // Change the internal state only when the transition actually starts
319                 onStateTransitionStart(state);
320             }
321 
322             @Override
323             public void onAnimationSuccess(Animator animator) {
324                 if (TestProtocol.sDebugTracing) {
325                     Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
326                 }
327                 onStateTransitionEnd(state);
328             }
329         });
330         mConfig.setAnimation(builder.buildAnim(), state);
331         return builder;
332     }
333 
onStateTransitionStart(STATE_TYPE state)334     private void onStateTransitionStart(STATE_TYPE state) {
335         mState = state;
336         mActivity.onStateSetStart(mState);
337 
338         for (int i = mListeners.size() - 1; i >= 0; i--) {
339             mListeners.get(i).onStateTransitionStart(state);
340         }
341     }
342 
onStateTransitionEnd(STATE_TYPE state)343     private void onStateTransitionEnd(STATE_TYPE state) {
344         // Only change the stable states after the transitions have finished
345         if (state != mCurrentStableState) {
346             mLastStableState = state.getHistoryForState(mCurrentStableState);
347             mCurrentStableState = state;
348         }
349 
350         mActivity.onStateSetEnd(state);
351         if (state == mBaseState) {
352             setRestState(null);
353         }
354 
355         for (int i = mListeners.size() - 1; i >= 0; i--) {
356             mListeners.get(i).onStateTransitionComplete(state);
357         }
358     }
359 
getLastState()360     public STATE_TYPE getLastState() {
361         return mLastStableState;
362     }
363 
moveToRestState()364     public void moveToRestState() {
365         if (mConfig.currentAnimation != null && mConfig.userControlled) {
366             // The user is doing something. Lets not mess it up
367             return;
368         }
369         if (mState.shouldDisableRestore()) {
370             goToState(getRestState());
371             // Reset history
372             mLastStableState = mBaseState;
373         }
374     }
375 
getRestState()376     public STATE_TYPE getRestState() {
377         return mRestState == null ? mBaseState : mRestState;
378     }
379 
setRestState(STATE_TYPE restState)380     public void setRestState(STATE_TYPE restState) {
381         mRestState = restState;
382     }
383 
384     /**
385      * Cancels the current animation.
386      */
cancelAnimation()387     public void cancelAnimation() {
388         mConfig.reset();
389     }
390 
setCurrentUserControlledAnimation(AnimatorPlaybackController controller)391     public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
392         clearCurrentAnimation();
393         setCurrentAnimation(controller.getTarget());
394         mConfig.userControlled = true;
395         mConfig.playbackController = controller;
396     }
397 
398     /**
399      * Sets the animation as the current state animation, i.e., canceled when
400      * starting another animation and may block some launcher interactions while running.
401      *
402      * @param childAnimations Set of animations with the new target is controlling.
403      */
setCurrentAnimation(AnimatorSet anim, Animator... childAnimations)404     public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
405         for (Animator childAnim : childAnimations) {
406             if (childAnim == null) {
407                 continue;
408             }
409             if (mConfig.playbackController != null
410                     && mConfig.playbackController.getTarget() == childAnim) {
411                 clearCurrentAnimation();
412                 break;
413             } else if (mConfig.currentAnimation == childAnim) {
414                 clearCurrentAnimation();
415                 break;
416             }
417         }
418         boolean reapplyNeeded = mConfig.currentAnimation != null;
419         cancelAnimation();
420         if (reapplyNeeded) {
421             reapplyState();
422             // Dispatch on transition end, so that any transient property is cleared.
423             onStateTransitionEnd(mState);
424         }
425         mConfig.setAnimation(anim, null);
426     }
427 
428     /**
429      * Cancels a currently running gesture animation
430      */
cancelStateElementAnimation(int index)431     public void cancelStateElementAnimation(int index) {
432         if (mAtomicAnimationFactory.mStateElementAnimators[index] != null) {
433             mAtomicAnimationFactory.mStateElementAnimators[index].cancel();
434         }
435     }
436 
createStateElementAnimation(int index, float... values)437     public Animator createStateElementAnimation(int index, float... values) {
438         cancelStateElementAnimation(index);
439         Animator anim = mAtomicAnimationFactory.createStateElementAnimation(index, values);
440         mAtomicAnimationFactory.mStateElementAnimators[index] = anim;
441         anim.addListener(new AnimatorListenerAdapter() {
442             @Override
443             public void onAnimationEnd(Animator animation) {
444                 mAtomicAnimationFactory.mStateElementAnimators[index] = null;
445             }
446         });
447         return anim;
448     }
449 
clearCurrentAnimation()450     private void clearCurrentAnimation() {
451         if (mConfig.currentAnimation != null) {
452             mConfig.currentAnimation.removeListener(mConfig);
453             mConfig.currentAnimation = null;
454         }
455         mConfig.playbackController = null;
456     }
457 
458     private class StartAnimRunnable implements Runnable {
459 
460         private final AnimatorSet mAnim;
461 
StartAnimRunnable(AnimatorSet anim)462         public StartAnimRunnable(AnimatorSet anim) {
463             mAnim = anim;
464         }
465 
466         @Override
run()467         public void run() {
468             if (mConfig.currentAnimation != mAnim) {
469                 return;
470             }
471             mAnim.start();
472         }
473     }
474 
475     private static class AnimationState<STATE_TYPE> extends StateAnimationConfig
476             implements AnimatorListener {
477 
478         private static final StateAnimationConfig DEFAULT = new StateAnimationConfig();
479 
480         public AnimatorPlaybackController playbackController;
481         public AnimatorSet currentAnimation;
482         public STATE_TYPE targetState;
483 
484         // Id to keep track of config changes, to tie an animation with the corresponding request
485         public int changeId = 0;
486 
487         /**
488          * Cancels the current animation and resets config variables.
489          */
reset()490         public void reset() {
491             DEFAULT.copyTo(this);
492             targetState = null;
493 
494             if (playbackController != null) {
495                 playbackController.getAnimationPlayer().cancel();
496                 playbackController.dispatchOnCancel();
497             } else if (currentAnimation != null) {
498                 currentAnimation.setDuration(0);
499                 currentAnimation.cancel();
500             }
501 
502             currentAnimation = null;
503             playbackController = null;
504             changeId++;
505         }
506 
507         @Override
onAnimationEnd(Animator animation)508         public void onAnimationEnd(Animator animation) {
509             if (playbackController != null && playbackController.getTarget() == animation) {
510                 playbackController = null;
511             }
512             if (currentAnimation == animation) {
513                 currentAnimation = null;
514             }
515         }
516 
setAnimation(AnimatorSet animation, STATE_TYPE targetState)517         public void setAnimation(AnimatorSet animation, STATE_TYPE targetState) {
518             currentAnimation = animation;
519             this.targetState = targetState;
520             currentAnimation.addListener(this);
521         }
522 
523         @Override
onAnimationStart(Animator animator)524         public void onAnimationStart(Animator animator) { }
525 
526         @Override
onAnimationCancel(Animator animator)527         public void onAnimationCancel(Animator animator) { }
528 
529         @Override
onAnimationRepeat(Animator animator)530         public void onAnimationRepeat(Animator animator) { }
531     }
532 
533     public interface StateHandler<STATE_TYPE> {
534 
535         /**
536          * Updates the UI to {@param state} without any animations
537          */
setState(STATE_TYPE state)538         void setState(STATE_TYPE state);
539 
540         /**
541          * Sets the UI to {@param state} by animating any changes.
542          */
setStateWithAnimation( STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation)543         void setStateWithAnimation(
544                 STATE_TYPE toState, StateAnimationConfig config, PendingAnimation animation);
545     }
546 
547     public interface StateListener<STATE_TYPE> {
548 
onStateTransitionStart(STATE_TYPE toState)549         default void onStateTransitionStart(STATE_TYPE toState) { }
550 
onStateTransitionComplete(STATE_TYPE finalState)551         default void onStateTransitionComplete(STATE_TYPE finalState) { }
552     }
553 
554     /**
555      * Factory class to configure and create atomic animations.
556      */
557     public static class AtomicAnimationFactory<STATE_TYPE> {
558 
559         protected static final int NEXT_INDEX = 0;
560 
561         private final Animator[] mStateElementAnimators;
562 
563         /**
564          *
565          * @param sharedElementAnimCount number of animations which run on state properties
566          */
AtomicAnimationFactory(int sharedElementAnimCount)567         public AtomicAnimationFactory(int sharedElementAnimCount) {
568             mStateElementAnimators = new Animator[sharedElementAnimCount];
569         }
570 
cancelAllStateElementAnimation()571         void cancelAllStateElementAnimation() {
572             for (Animator animator : mStateElementAnimators) {
573                 if (animator != null) {
574                     animator.cancel();
575                 }
576             }
577         }
578 
579         /**
580          * Creates animations for elements which can be also be part of state transitions. The
581          * actual definition of the animation is up to the app to define.
582          *
583          */
createStateElementAnimation(int index, float... values)584         public Animator createStateElementAnimation(int index, float... values) {
585             throw new RuntimeException("Unknown gesture animation " + index);
586         }
587 
588         /**
589          * Prepares for a non-user controlled animation from fromState to this state. Preparations
590          * include:
591          * - Setting interpolators for various animations included in the state transition.
592          * - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
593          */
prepareForAtomicAnimation( STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config)594         public void prepareForAtomicAnimation(
595                 STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) { }
596     }
597 }
598