• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019, 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 package com.android.managedprovisioning.provisioning;
17 
18 import static java.util.Objects.requireNonNull;
19 
20 import android.annotation.StringRes;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.ImageView;
27 import android.widget.LinearLayout;
28 import android.widget.Space;
29 import android.widget.TextView;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.managedprovisioning.R;
33 import com.android.managedprovisioning.common.CrossFadeHelper;
34 import com.android.managedprovisioning.common.CrossFadeHelper.Callback;
35 import com.android.managedprovisioning.common.StylerHelper;
36 import com.android.managedprovisioning.provisioning.ProvisioningModeWrapperProvider.ProvisioningModeWrapper;
37 
38 import com.airbnb.lottie.LottieAnimationView;
39 
40 import java.util.Arrays;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * Handles the animated transitions in the education screens. Transitions consist of cross fade
46  * animations between different headers and banner images.
47  */
48 class TransitionAnimationHelper {
49 
50     interface TransitionAnimationCallback {
onAllTransitionsShown()51         void onAllTransitionsShown();
52 
onAnimationSetup(LottieAnimationView animationView)53         void onAnimationSetup(LottieAnimationView animationView);
54 
55     }
56     interface TransitionAnimationStateManager {
saveState(TransitionAnimationState state)57         void saveState(TransitionAnimationState state);
58 
restoreState()59         TransitionAnimationState restoreState();
60     }
61 
62     private static final int TRANSITION_TIME_MILLIS = 5000;
63     private static final int CROSSFADE_ANIMATION_DURATION_MILLIS = 500;
64 
65     private final CrossFadeHelper mCrossFadeHelper;
66     private final AnimationComponents mAnimationComponents;
67     private final Runnable mStartNextTransitionRunnable = this::startNextAnimation;
68     private final boolean mShowAnimations;
69     private TransitionAnimationCallback mCallback;
70     private TransitionAnimationStateManager mStateManager;
71     private final ProvisioningModeWrapper mProvisioningModeWrapper;
72 
73     private Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
74     private TransitionAnimationState mTransitionAnimationState;
75     private final StylerHelper mStylerHelper;
76 
TransitionAnimationHelper(AnimationComponents animationComponents, TransitionAnimationCallback callback, TransitionAnimationStateManager stateManager, StylerHelper stylerHelper, ProvisioningModeWrapper provisioningModeWrapper)77     TransitionAnimationHelper(AnimationComponents animationComponents,
78             TransitionAnimationCallback callback,
79             TransitionAnimationStateManager stateManager,
80             StylerHelper stylerHelper,
81             ProvisioningModeWrapper provisioningModeWrapper) {
82         mAnimationComponents = requireNonNull(animationComponents);
83         mCallback = requireNonNull(callback);
84         mStateManager = requireNonNull(stateManager);
85         mProvisioningModeWrapper = provisioningModeWrapper;
86         mCrossFadeHelper = getCrossFadeHelper();
87         mShowAnimations = shouldShowAnimations();
88         mStylerHelper = requireNonNull(stylerHelper);
89 
90         applyContentDescription(
91                 mAnimationComponents.mAnimationView,
92                 mProvisioningModeWrapper.summary);
93     }
94 
areAllTransitionsShown()95     boolean areAllTransitionsShown() {
96         return mTransitionAnimationState.mAnimationIndex
97                 == mProvisioningModeWrapper.transitions.length - 1;
98     }
99 
start()100     void start() {
101         mTransitionAnimationState = maybeRestoreState();
102         scheduleNextTransition(getTimeLeftForTransition(mTransitionAnimationState));
103         updateUiValues(mTransitionAnimationState.mAnimationIndex);
104         startCurrentAnimatedDrawable(mTransitionAnimationState.mProgress);
105     }
106 
getTimeLeftForTransition(TransitionAnimationState transitionAnimationState)107     private long getTimeLeftForTransition(TransitionAnimationState transitionAnimationState) {
108         long timeSinceLastTransition =
109                 System.currentTimeMillis() - transitionAnimationState.mLastTransitionTimestamp;
110         return TRANSITION_TIME_MILLIS - timeSinceLastTransition;
111     }
112 
stop()113     void stop() {
114         updateState();
115         mStateManager.saveState(mTransitionAnimationState);
116         clean();
117     }
118 
updateState()119     private void updateState() {
120         mTransitionAnimationState.mProgress = mAnimationComponents.mAnimationView.getProgress();
121     }
122 
maybeRestoreState()123     private TransitionAnimationState maybeRestoreState() {
124         TransitionAnimationState transitionAnimationState = mStateManager.restoreState();
125         if (transitionAnimationState == null) {
126             return new TransitionAnimationState(
127                     /* animationIndex */ 0,
128                     /* progress */ 0,
129                     /* lastTransitionTimestamp */ System.currentTimeMillis());
130         }
131         return transitionAnimationState;
132     }
133 
clean()134     private void clean() {
135         stopCurrentAnimatedDrawable();
136         mCrossFadeHelper.cleanup();
137         mUiThreadHandler.removeCallbacksAndMessages(null);
138         mUiThreadHandler = null;
139         mCallback = null;
140         mStateManager = null;
141     }
142 
143     @VisibleForTesting
getCrossFadeHelper()144     CrossFadeHelper getCrossFadeHelper() {
145         return new CrossFadeHelper(
146             mAnimationComponents.asList(),
147             CROSSFADE_ANIMATION_DURATION_MILLIS,
148             new Callback() {
149                 @Override
150                 public void fadeOutCompleted() {
151                     stopCurrentAnimatedDrawable();
152                     mTransitionAnimationState.mAnimationIndex++;
153                     updateUiValues(mTransitionAnimationState.mAnimationIndex);
154                     startCurrentAnimatedDrawable(/* startProgress */ 0f);
155                 }
156 
157                 @Override
158                 public void fadeInCompleted() {
159                     mTransitionAnimationState.mLastTransitionTimestamp = System.currentTimeMillis();
160                     scheduleNextTransition(TRANSITION_TIME_MILLIS);
161                 }
162             });
163     }
164 
165     private void scheduleNextTransition(long timeLeftForTransition) {
166         mUiThreadHandler.postDelayed(mStartNextTransitionRunnable, timeLeftForTransition);
167     }
168 
169     @VisibleForTesting
170     void startNextAnimation() {
171         if (mTransitionAnimationState.mAnimationIndex
172                 >= mProvisioningModeWrapper.transitions.length - 1) {
173             if (mCallback != null) {
174                 mCallback.onAllTransitionsShown();
175             }
176             return;
177         }
178         mCrossFadeHelper.start();
179     }
180 
181     @VisibleForTesting
182     void startCurrentAnimatedDrawable(float startProgress) {
183         if (!mShowAnimations) {
184             return;
185         }
186         boolean shouldLoop =
187                 getTransitionForIndex(mTransitionAnimationState.mAnimationIndex).shouldLoop;
188         mAnimationComponents.mAnimationView.loop(shouldLoop);
189         mAnimationComponents.mAnimationView.setProgress(startProgress);
190         mAnimationComponents.mAnimationView.playAnimation();
191     }
192 
193     @VisibleForTesting
194     void stopCurrentAnimatedDrawable() {
195         if (!mShowAnimations) {
196             return;
197         }
198         mAnimationComponents.mAnimationView.pauseAnimation();
199     }
200 
201     @VisibleForTesting
202     void updateUiValues(int currentTransitionIndex) {
203         final TransitionScreenWrapper transition =
204                 getTransitionForIndex(currentTransitionIndex);
205         setupHeaderText(transition);
206         setupDescriptionText(transition);
207         setupAnimation(transition);
208 
209         boolean isTextBasedEduScreen = transition.subHeaderIcon != 0;
210         updateItemValues(
211                 mAnimationComponents.mItem1,
212                 transition.subHeaderIcon,
213                 transition.subHeaderTitle,
214                 transition.subHeader,
215                 isTextBasedEduScreen);
216         updateItemValues(
217                 mAnimationComponents.mItem2,
218                 transition.secondarySubHeaderIcon,
219                 transition.secondarySubHeaderTitle,
220                 transition.secondarySubHeader,
221                 isTextBasedEduScreen);
222         updateSpaceVisibility(mAnimationComponents.mSpace1, isTextBasedEduScreen);
223         updateSpaceVisibility(mAnimationComponents.mSpace2, isTextBasedEduScreen);
224     }
225 
226     private void setupHeaderText(TransitionScreenWrapper transition) {
227         mAnimationComponents.mHeader.setText(transition.header);
228         triggerTextToSpeechIfFocused(mAnimationComponents.mHeader);
229     }
230 
231     private void triggerTextToSpeechIfFocused(TextView view) {
232         if (view.isAccessibilityFocused()) {
233             view.announceForAccessibility(view.getText().toString());
234         }
235     }
236 
237     private void setupAnimation(TransitionScreenWrapper transition) {
238         if (mShowAnimations && transition.drawable != 0) {
239             mAnimationComponents.mAnimationView.setAnimation(transition.drawable);
240             mCallback.onAnimationSetup(mAnimationComponents.mAnimationView);
241             mAnimationComponents.mImageContainer.setVisibility(View.VISIBLE);
242         } else {
243             mAnimationComponents.mImageContainer.setVisibility(View.GONE);
244         }
245     }
246 
247     private void setupDescriptionText(TransitionScreenWrapper transition) {
248         if (transition.description != 0) {
249             mAnimationComponents.mDescription.setText(transition.description);
250             mAnimationComponents.mDescription.setVisibility(View.VISIBLE);
251             triggerTextToSpeechIfFocused(mAnimationComponents.mDescription);
252         } else {
253             mAnimationComponents.mDescription.setVisibility(View.GONE);
254         }
255     }
256 
257     private void updateItemValues(ViewGroup item, int icon, int subHeaderTitle, int subHeader,
258             boolean isTextBasedEduScreen) {
259         if (isTextBasedEduScreen) {
260             ((ImageView) item.findViewById(R.id.sud_items_icon)).setImageResource(icon);
261             ((TextView) item.findViewById(R.id.sud_items_title)).setText(subHeaderTitle);
262             ((TextView) item.findViewById(R.id.sud_items_summary)).setText(subHeader);
263             mStylerHelper.applyListItemStyling(
264                     item, new LinearLayout.LayoutParams(item.getLayoutParams()));
265             item.setVisibility(View.VISIBLE);
266         } else {
267             item.setVisibility(View.GONE);
268         }
269     }
270 
271     private void updateSpaceVisibility(Space space, boolean isTextBasedEduScreen) {
272         if (isTextBasedEduScreen) {
273             space.setVisibility(View.VISIBLE);
274         } else {
275             space.setVisibility(View.GONE);
276         }
277     }
278 
279     private TransitionScreenWrapper getTransitionForIndex(int currentTransitionIndex) {
280         TransitionScreenWrapper[] transitions = mProvisioningModeWrapper.transitions;
281         return transitions[currentTransitionIndex % transitions.length];
282     }
283 
284     private boolean shouldShowAnimations() {
285         final Context context = mAnimationComponents.mHeader.getContext();
286         return context.getResources().getBoolean(R.bool.show_edu_animations);
287     }
288 
289     private void applyContentDescription(View view, @StringRes int summaryRes) {
290         Context context = view.getContext();
291         view.setContentDescription(context.getString(summaryRes));
292     }
293 
294     static final class AnimationComponents {
295         private final TextView mHeader;
296         private final TextView mDescription;
297         private final LottieAnimationView mAnimationView;
298         private final ViewGroup mImageContainer;
299         private final ViewGroup mItem1;
300         private final ViewGroup mItem2;
301         private final Space mSpace1;
302         private final Space mSpace2;
303 
304         AnimationComponents(TextView header, TextView description, ViewGroup item1,
305                 ViewGroup item2, LottieAnimationView animationView, ViewGroup imageContainer,
306                 Space space1, Space space2) {
307             this.mHeader = requireNonNull(header);
308             this.mDescription = requireNonNull(description);
309             this.mItem1 = requireNonNull(item1);
310             this.mItem2 = requireNonNull(item2);
311             this.mImageContainer = requireNonNull(imageContainer);
312             this.mAnimationView = requireNonNull(animationView);
313             this.mSpace1 = requireNonNull(space1);
314             this.mSpace2 = requireNonNull(space2);
315         }
316 
317         List<View> asList() {
318             return Arrays.asList(mHeader, mItem1, mItem2, mImageContainer);
319         }
320     }
321 
322     static final class TransitionAnimationState {
323         private int mAnimationIndex;
324         private float mProgress;
325         private long mLastTransitionTimestamp;
326 
327         TransitionAnimationState(
328                 int animationIndex,
329                 float progress,
330                 long lastTransitionTimestamp) {
331             mAnimationIndex = animationIndex;
332             mProgress = progress;
333             mLastTransitionTimestamp = lastTransitionTimestamp;
334         }
335 
336         @Override
337         public boolean equals(Object o) {
338             if (this == o) return true;
339             if (!(o instanceof TransitionAnimationState)) return false;
340             TransitionAnimationState that = (TransitionAnimationState) o;
341             return mAnimationIndex == that.mAnimationIndex &&
342                     Float.compare(that.mProgress, mProgress) == 0 &&
343                     mLastTransitionTimestamp == that.mLastTransitionTimestamp;
344         }
345 
346         @Override
347         public int hashCode() {
348             return Objects.hash(mAnimationIndex, mProgress, mLastTransitionTimestamp);
349         }
350     }
351 }
352