• 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.content.Context;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.ImageView;
26 import android.widget.LinearLayout;
27 import android.widget.Space;
28 import android.widget.TextView;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.managedprovisioning.R;
32 import com.android.managedprovisioning.common.CrossFadeHelper;
33 import com.android.managedprovisioning.common.CrossFadeHelper.Callback;
34 import com.android.managedprovisioning.common.StylerHelper;
35 import com.android.managedprovisioning.provisioning.ProvisioningModeWrapperProvider.ProvisioningModeWrapper;
36 import com.android.managedprovisioning.util.LazyStringResource;
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(
78             AnimationComponents animationComponents,
79             TransitionAnimationCallback callback,
80             TransitionAnimationStateManager stateManager,
81             StylerHelper stylerHelper,
82             ProvisioningModeWrapper provisioningModeWrapper) {
83         mAnimationComponents = requireNonNull(animationComponents);
84         mCallback = requireNonNull(callback);
85         mStateManager = requireNonNull(stateManager);
86         mProvisioningModeWrapper = provisioningModeWrapper;
87         mCrossFadeHelper = getCrossFadeHelper();
88         mShowAnimations = shouldShowAnimations();
89         mStylerHelper = requireNonNull(stylerHelper);
90 
91         applyContentDescription(
92                 mAnimationComponents.mAnimationView, mProvisioningModeWrapper.mSummary);
93     }
94 
areAllTransitionsShown()95     boolean areAllTransitionsShown() {
96         return mTransitionAnimationState.mAnimationIndex
97                 == mProvisioningModeWrapper.mTransitions.size() - 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.mTransitions.size() - 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         var context = mAnimationComponents.mHeader.getContext();
228         mAnimationComponents.mHeader.setText(transition.header.value(context));
229         triggerTextToSpeechIfFocused(mAnimationComponents.mHeader);
230     }
231 
232     private void triggerTextToSpeechIfFocused(TextView view) {
233         if (view.isAccessibilityFocused()) {
234             view.announceForAccessibility(view.getText().toString());
235         }
236     }
237 
238     private void setupAnimation(TransitionScreenWrapper transition) {
239         if (mShowAnimations && transition.drawable != 0) {
240             mAnimationComponents.mAnimationView.setAnimation(transition.drawable);
241             mCallback.onAnimationSetup(mAnimationComponents.mAnimationView);
242             mAnimationComponents.mImageContainer.setVisibility(View.VISIBLE);
243         } else {
244             mAnimationComponents.mImageContainer.setVisibility(View.GONE);
245         }
246     }
247 
248     private void setupDescriptionText(TransitionScreenWrapper transition) {
249         var context = mAnimationComponents.mDescription.getContext();
250         if (transition.description.isBlank(context)) {
251             mAnimationComponents.mDescription.setText(transition.description.value(context));
252             mAnimationComponents.mDescription.setVisibility(View.VISIBLE);
253             triggerTextToSpeechIfFocused(mAnimationComponents.mDescription);
254         } else {
255             mAnimationComponents.mDescription.setVisibility(View.GONE);
256         }
257     }
258 
259     private void updateItemValues(
260             ViewGroup item,
261             int icon,
262             LazyStringResource subHeaderTitle,
263             LazyStringResource subHeader,
264             boolean isTextBasedEduScreen) {
265         var context = item.getContext();
266         if (isTextBasedEduScreen) {
267             ((ImageView) item.findViewById(R.id.sud_items_icon)).setImageResource(icon);
268             ((TextView) item.findViewById(R.id.sud_items_title)).setText(
269                     subHeaderTitle.value(context));
270             ((TextView) item.findViewById(R.id.sud_items_summary)).setText(
271                     subHeader.value(context));
272             mStylerHelper.applyListItemStyling(
273                     item, new LinearLayout.LayoutParams(item.getLayoutParams()));
274             item.setVisibility(View.VISIBLE);
275         } else {
276             item.setVisibility(View.GONE);
277         }
278     }
279 
280     private void updateSpaceVisibility(Space space, boolean isTextBasedEduScreen) {
281         if (isTextBasedEduScreen) {
282             space.setVisibility(View.VISIBLE);
283         } else {
284             space.setVisibility(View.GONE);
285         }
286     }
287 
288     private TransitionScreenWrapper getTransitionForIndex(int currentTransitionIndex) {
289         var transitions = mProvisioningModeWrapper.mTransitions;
290         return transitions.get(currentTransitionIndex % transitions.size());
291     }
292 
293     private boolean shouldShowAnimations() {
294         final Context context = mAnimationComponents.mHeader.getContext();
295         return context.getResources().getBoolean(R.bool.show_edu_animations);
296     }
297 
298     private void applyContentDescription(View view, LazyStringResource summaryRes) {
299         view.setContentDescription(summaryRes.value(view.getContext()));
300     }
301 
302     static final class AnimationComponents {
303         private final TextView mHeader;
304         private final TextView mDescription;
305         private final LottieAnimationView mAnimationView;
306         private final ViewGroup mImageContainer;
307         private final ViewGroup mItem1;
308         private final ViewGroup mItem2;
309         private final Space mSpace1;
310         private final Space mSpace2;
311 
312         AnimationComponents(TextView header, TextView description, ViewGroup item1,
313                 ViewGroup item2, LottieAnimationView animationView, ViewGroup imageContainer,
314                 Space space1, Space space2) {
315             this.mHeader = requireNonNull(header);
316             this.mDescription = requireNonNull(description);
317             this.mItem1 = requireNonNull(item1);
318             this.mItem2 = requireNonNull(item2);
319             this.mImageContainer = requireNonNull(imageContainer);
320             this.mAnimationView = requireNonNull(animationView);
321             this.mSpace1 = requireNonNull(space1);
322             this.mSpace2 = requireNonNull(space2);
323         }
324 
325         List<View> asList() {
326             return Arrays.asList(mHeader, mItem1, mItem2, mImageContainer);
327         }
328     }
329 
330     static final class TransitionAnimationState {
331         private int mAnimationIndex;
332         private float mProgress;
333         private long mLastTransitionTimestamp;
334 
335         TransitionAnimationState(
336                 int animationIndex,
337                 float progress,
338                 long lastTransitionTimestamp) {
339             mAnimationIndex = animationIndex;
340             mProgress = progress;
341             mLastTransitionTimestamp = lastTransitionTimestamp;
342         }
343 
344         @Override
345         public boolean equals(Object o) {
346             if (this == o) return true;
347             if (!(o instanceof TransitionAnimationState)) return false;
348             TransitionAnimationState that = (TransitionAnimationState) o;
349             return mAnimationIndex == that.mAnimationIndex &&
350                     Float.compare(that.mProgress, mProgress) == 0 &&
351                     mLastTransitionTimestamp == that.mLastTransitionTimestamp;
352         }
353 
354         @Override
355         public int hashCode() {
356             return Objects.hash(mAnimationIndex, mProgress, mLastTransitionTimestamp);
357         }
358     }
359 }
360