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 com.android.internal.util.Preconditions.checkNotNull;
19 import static com.android.managedprovisioning.provisioning.ProvisioningActivity.PROVISIONING_MODE_FULLY_MANAGED_DEVICE;
20 import static com.android.managedprovisioning.provisioning.ProvisioningActivity.PROVISIONING_MODE_WORK_PROFILE;
21 import static com.android.managedprovisioning.provisioning.ProvisioningActivity.PROVISIONING_MODE_WORK_PROFILE_ON_FULLY_MANAGED_DEVICE;
22 
23 import android.annotation.DrawableRes;
24 import android.annotation.StringRes;
25 import android.content.Context;
26 import android.graphics.drawable.AnimatedVectorDrawable;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.view.View;
30 import android.widget.ImageView;
31 import android.widget.TextView;
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.managedprovisioning.R;
34 import com.android.managedprovisioning.common.CrossFadeHelper;
35 import com.android.managedprovisioning.common.CrossFadeHelper.Callback;
36 import com.android.managedprovisioning.common.RepeatingVectorAnimation;
37 import com.android.managedprovisioning.provisioning.ProvisioningActivity.ProvisioningMode;
38 import java.util.Arrays;
39 import java.util.List;
40 
41 /**
42  * Handles the animated transitions in the education screens. Transitions consist of cross fade
43  * animations between different headers and banner images.
44  */
45 class TransitionAnimationHelper {
46 
47     interface TransitionAnimationCallback {
onAllTransitionsShown()48         void onAllTransitionsShown();
49     }
50 
51     @VisibleForTesting
52     static final ProvisioningModeWrapper WORK_PROFILE_WRAPPER
53             = new ProvisioningModeWrapper(new TransitionScreenWrapper[] {
54         new TransitionScreenWrapper(R.string.work_profile_provisioning_step_1_header,
55                 R.drawable.separate_work_and_personal_animation),
56         new TransitionScreenWrapper(R.string.work_profile_provisioning_step_2_header,
57                 R.drawable.pause_work_apps_animation),
58         new TransitionScreenWrapper(R.string.work_profile_provisioning_step_3_header,
59                 R.drawable.not_private_animation)
60     }, R.string.work_profile_provisioning_summary);
61 
62     @VisibleForTesting
63     static final ProvisioningModeWrapper FULLY_MANAGED_DEVICE_WRAPPER
64             = new ProvisioningModeWrapper(new TransitionScreenWrapper[] {
65         new TransitionScreenWrapper(R.string.fully_managed_device_provisioning_step_1_header,
66                 R.drawable.connect_on_the_go_animation),
67         new TransitionScreenWrapper(R.string.fully_managed_device_provisioning_step_2_header,
68                 R.drawable.not_private_animation,
69                 R.string.fully_managed_device_provisioning_step_2_subheader,
70                 /* showContactAdmin */ true)
71     }, R.string.fully_managed_device_provisioning_summary);
72 
73     private static final int TRANSITION_TIME_MILLIS = 5000;
74     private static final int CROSSFADE_ANIMATION_DURATION_MILLIS = 500;
75 
76     private final CrossFadeHelper mCrossFadeHelper;
77     private final AnimationComponents mAnimationComponents;
78     private final Runnable mStartNextTransitionRunnable = this::startNextAnimation;
79     private final boolean mShowAnimations;
80     private TransitionAnimationCallback mCallback;
81     private final ProvisioningModeWrapper mProvisioningModeWrapper;
82 
83     private Handler mUiThreadHandler = new Handler(Looper.getMainLooper());
84     private int mCurrentTransitionIndex;
85     private RepeatingVectorAnimation mRepeatingVectorAnimation;
86 
TransitionAnimationHelper(@rovisioningMode int provisioningMode, AnimationComponents animationComponents, TransitionAnimationCallback callback)87     TransitionAnimationHelper(@ProvisioningMode int provisioningMode,
88             AnimationComponents animationComponents, TransitionAnimationCallback callback) {
89         mAnimationComponents = checkNotNull(animationComponents);
90         mCallback = checkNotNull(callback);
91         mProvisioningModeWrapper = getProvisioningModeWrapper(provisioningMode);
92         mCrossFadeHelper = getCrossFadeHelper();
93         mShowAnimations = shouldShowAnimations();
94 
95         applyContentDescription();
96         updateUiValues(mCurrentTransitionIndex);
97     }
98 
areAllTransitionsShown()99     boolean areAllTransitionsShown() {
100         return mCurrentTransitionIndex == mProvisioningModeWrapper.transitions.length - 1;
101     }
102 
start()103     void start() {
104         mUiThreadHandler.postDelayed(mStartNextTransitionRunnable, TRANSITION_TIME_MILLIS);
105         updateUiValues(mCurrentTransitionIndex);
106         startCurrentAnimatedDrawable();
107     }
108 
clean()109     void clean() {
110         stopCurrentAnimatedDrawable();
111         mCrossFadeHelper.cleanup();
112         mUiThreadHandler.removeCallbacksAndMessages(null);
113         mUiThreadHandler = null;
114         mCallback = null;
115     }
116 
117     @VisibleForTesting
getCrossFadeHelper()118     CrossFadeHelper getCrossFadeHelper() {
119         return new CrossFadeHelper(
120             mAnimationComponents.asList(),
121             CROSSFADE_ANIMATION_DURATION_MILLIS,
122             new Callback() {
123                 @Override
124                 public void fadeOutCompleted() {
125                     stopCurrentAnimatedDrawable();
126                     mCurrentTransitionIndex++;
127                     updateUiValues(mCurrentTransitionIndex);
128                     startCurrentAnimatedDrawable();
129                 }
130 
131                 @Override
132                 public void fadeInCompleted() {
133                     mUiThreadHandler.postDelayed(
134                         mStartNextTransitionRunnable, TRANSITION_TIME_MILLIS);
135                 }
136             });
137     }
138 
139     @VisibleForTesting
140     void startNextAnimation() {
141         if (mCurrentTransitionIndex >= mProvisioningModeWrapper.transitions.length-1) {
142             if (mCallback != null) {
143                 mCallback.onAllTransitionsShown();
144             }
145             return;
146         }
147         mCrossFadeHelper.start();
148     }
149 
150     @VisibleForTesting
151     void startCurrentAnimatedDrawable() {
152         if (!mShowAnimations) {
153             return;
154         }
155         if (!(mAnimationComponents.image.getDrawable() instanceof AnimatedVectorDrawable)) {
156             return;
157         }
158         final AnimatedVectorDrawable vectorDrawable =
159             (AnimatedVectorDrawable) mAnimationComponents.image.getDrawable();
160         mRepeatingVectorAnimation = new RepeatingVectorAnimation(vectorDrawable);
161         mRepeatingVectorAnimation.start();
162     }
163 
164     @VisibleForTesting
165     void stopCurrentAnimatedDrawable() {
166         if (!mShowAnimations) {
167             return;
168         }
169         if (!(mAnimationComponents.image.getDrawable() instanceof AnimatedVectorDrawable)) {
170             return;
171         }
172         mRepeatingVectorAnimation.stop();
173     }
174 
175     @VisibleForTesting
176     void updateUiValues(int currentTransitionIndex) {
177         final TransitionScreenWrapper[] transitions = mProvisioningModeWrapper.transitions;
178         final TransitionScreenWrapper transition =
179                 transitions[currentTransitionIndex % transitions.length];
180 
181         mAnimationComponents.header.setText(transition.header);
182 
183         final ImageView image = mAnimationComponents.image;
184         if (mShowAnimations) {
185             image.setImageResource(transition.drawable);
186         } else {
187             image.setVisibility(View.GONE);
188         }
189 
190         final TextView subHeader = mAnimationComponents.subHeader;
191         if (transition.subHeader != 0) {
192             subHeader.setVisibility(View.VISIBLE);
193             subHeader.setText(transition.subHeader);
194         } else {
195             subHeader.setVisibility(View.INVISIBLE);
196         }
197 
198         final TextView providerInfo = mAnimationComponents.providerInfo;
199         if (transition.showContactAdmin) {
200             providerInfo.setVisibility(View.VISIBLE);
201         } else {
202             providerInfo.setVisibility(View.INVISIBLE);
203         }
204     }
205 
206     @VisibleForTesting
207     ProvisioningModeWrapper getProvisioningModeWrapper(
208             @ProvisioningMode int provisioningMode) {
209         switch (provisioningMode) {
210             case PROVISIONING_MODE_WORK_PROFILE:
211                 return WORK_PROFILE_WRAPPER;
212             case PROVISIONING_MODE_FULLY_MANAGED_DEVICE:
213                 return FULLY_MANAGED_DEVICE_WRAPPER;
214         }
215         throw new IllegalStateException("Unexpected provisioning mode " + provisioningMode);
216     }
217 
218     private boolean shouldShowAnimations() {
219         final Context context = mAnimationComponents.header.getContext();
220         return context.getResources().getBoolean(R.bool.show_edu_animations);
221     }
222 
223     private void applyContentDescription() {
224         final TextView header = mAnimationComponents.header;
225         final Context context = header.getContext();
226         header.setContentDescription(context.getString(mProvisioningModeWrapper.summary));
227     }
228 
229     private static final class TransitionScreenWrapper {
230         final @StringRes int header;
231         final @DrawableRes int drawable;
232         final @StringRes int subHeader;
233         final boolean showContactAdmin;
234 
235         TransitionScreenWrapper(@StringRes int header, @DrawableRes int drawable) {
236             this(header, drawable, /* subHeader */ 0, /* showContactAdmin */ false);
237         }
238 
239         TransitionScreenWrapper(@StringRes int header, @DrawableRes int drawable,
240                 @StringRes int subHeader, boolean showContactAdmin) {
241             this.header = checkNotNull(header,
242                     "Header resource id must be a positive number.");
243             this.drawable = checkNotNull(drawable,
244                     "Drawable resource id must be a positive number.");
245             this.subHeader = subHeader;
246             this.showContactAdmin = showContactAdmin;
247         }
248     }
249 
250     private static final class ProvisioningModeWrapper {
251         final TransitionScreenWrapper[] transitions;
252         final @StringRes int summary;
253 
254         ProvisioningModeWrapper(TransitionScreenWrapper[] transitions, @StringRes int summary) {
255             this.transitions = checkNotNull(transitions);
256             this.summary = summary;
257         }
258     }
259 
260     static final class AnimationComponents {
261         private final TextView header;
262         private final TextView subHeader;
263         private final ImageView image;
264         private final TextView providerInfo;
265 
266         AnimationComponents(
267                 TextView header, TextView subHeader, ImageView image, TextView providerInfo) {
268             this.header = checkNotNull(header);
269             this.subHeader = checkNotNull(subHeader);
270             this.image = checkNotNull(image);
271             this.providerInfo = checkNotNull(providerInfo);
272         }
273 
274         List<View> asList() {
275             return Arrays.asList(header, subHeader, image, providerInfo);
276         }
277     }
278 }
279