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