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_ORG_OWNED_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 /* shouldLoop */ true) 72 }, R.string.fully_managed_device_provisioning_summary); 73 74 @VisibleForTesting 75 static final ProvisioningModeWrapper WORK_PROFILE_ON_ORG_OWNED_DEVICE_WRAPPER 76 = new ProvisioningModeWrapper(new TransitionScreenWrapper[] { 77 new TransitionScreenWrapper(R.string.cope_provisioning_step_1_header, 78 R.drawable.separate_work_and_personal_animation), 79 new TransitionScreenWrapper(R.string.cope_provisioning_step_2_header, 80 R.drawable.personal_apps_separate_hidden_from_work_animation, 81 /* subHeader */ 0, 82 /* showContactAdmin */ false, 83 /* shouldLoop */ false), 84 new TransitionScreenWrapper(R.string.cope_provisioning_step_3_header, 85 R.drawable.it_admin_control_device_block_apps_animation) 86 }, R.string.cope_provisioning_summary); 87 88 private static final int TRANSITION_TIME_MILLIS = 5000; 89 private static final int CROSSFADE_ANIMATION_DURATION_MILLIS = 500; 90 91 private final CrossFadeHelper mCrossFadeHelper; 92 private final AnimationComponents mAnimationComponents; 93 private final Runnable mStartNextTransitionRunnable = this::startNextAnimation; 94 private final boolean mShowAnimations; 95 private TransitionAnimationCallback mCallback; 96 private final ProvisioningModeWrapper mProvisioningModeWrapper; 97 98 private Handler mUiThreadHandler = new Handler(Looper.getMainLooper()); 99 private int mCurrentTransitionIndex; 100 private RepeatingVectorAnimation mRepeatingVectorAnimation; 101 TransitionAnimationHelper(@rovisioningMode int provisioningMode, AnimationComponents animationComponents, TransitionAnimationCallback callback)102 TransitionAnimationHelper(@ProvisioningMode int provisioningMode, 103 AnimationComponents animationComponents, TransitionAnimationCallback callback) { 104 mAnimationComponents = checkNotNull(animationComponents); 105 mCallback = checkNotNull(callback); 106 mProvisioningModeWrapper = getProvisioningModeWrapper(provisioningMode); 107 mCrossFadeHelper = getCrossFadeHelper(); 108 mShowAnimations = shouldShowAnimations(); 109 110 applyContentDescription(); 111 updateUiValues(mCurrentTransitionIndex); 112 } 113 areAllTransitionsShown()114 boolean areAllTransitionsShown() { 115 return mCurrentTransitionIndex == mProvisioningModeWrapper.transitions.length - 1; 116 } 117 start()118 void start() { 119 mUiThreadHandler.postDelayed(mStartNextTransitionRunnable, TRANSITION_TIME_MILLIS); 120 updateUiValues(mCurrentTransitionIndex); 121 startCurrentAnimatedDrawable(); 122 } 123 clean()124 void clean() { 125 stopCurrentAnimatedDrawable(); 126 mCrossFadeHelper.cleanup(); 127 mUiThreadHandler.removeCallbacksAndMessages(null); 128 mUiThreadHandler = null; 129 mCallback = null; 130 } 131 132 @VisibleForTesting getCrossFadeHelper()133 CrossFadeHelper getCrossFadeHelper() { 134 return new CrossFadeHelper( 135 mAnimationComponents.asList(), 136 CROSSFADE_ANIMATION_DURATION_MILLIS, 137 new Callback() { 138 @Override 139 public void fadeOutCompleted() { 140 stopCurrentAnimatedDrawable(); 141 mCurrentTransitionIndex++; 142 updateUiValues(mCurrentTransitionIndex); 143 startCurrentAnimatedDrawable(); 144 } 145 146 @Override 147 public void fadeInCompleted() { 148 mUiThreadHandler.postDelayed( 149 mStartNextTransitionRunnable, TRANSITION_TIME_MILLIS); 150 } 151 }); 152 } 153 154 @VisibleForTesting 155 void startNextAnimation() { 156 if (mCurrentTransitionIndex >= mProvisioningModeWrapper.transitions.length-1) { 157 if (mCallback != null) { 158 mCallback.onAllTransitionsShown(); 159 } 160 return; 161 } 162 mCrossFadeHelper.start(); 163 } 164 165 @VisibleForTesting 166 void startCurrentAnimatedDrawable() { 167 if (!mShowAnimations) { 168 return; 169 } 170 if (!(mAnimationComponents.image.getDrawable() instanceof AnimatedVectorDrawable)) { 171 return; 172 } 173 final AnimatedVectorDrawable vectorDrawable = 174 (AnimatedVectorDrawable) mAnimationComponents.image.getDrawable(); 175 boolean shouldLoop = getTransitionForIndex(mCurrentTransitionIndex).shouldLoop; 176 mRepeatingVectorAnimation = new RepeatingVectorAnimation(vectorDrawable, shouldLoop); 177 mRepeatingVectorAnimation.start(); 178 } 179 180 @VisibleForTesting 181 void stopCurrentAnimatedDrawable() { 182 if (!mShowAnimations) { 183 return; 184 } 185 if (!(mAnimationComponents.image.getDrawable() instanceof AnimatedVectorDrawable)) { 186 return; 187 } 188 mRepeatingVectorAnimation.stop(); 189 } 190 191 @VisibleForTesting 192 void updateUiValues(int currentTransitionIndex) { 193 final TransitionScreenWrapper transition = 194 getTransitionForIndex(currentTransitionIndex); 195 196 mAnimationComponents.header.setText(transition.header); 197 198 final ImageView image = mAnimationComponents.image; 199 if (mShowAnimations) { 200 image.setImageResource(transition.drawable); 201 } else { 202 image.setVisibility(View.GONE); 203 } 204 205 final TextView subHeader = mAnimationComponents.subHeader; 206 if (transition.subHeader != 0) { 207 subHeader.setVisibility(View.VISIBLE); 208 subHeader.setText(transition.subHeader); 209 } else { 210 subHeader.setVisibility(View.INVISIBLE); 211 } 212 213 final TextView providerInfo = mAnimationComponents.providerInfo; 214 if (transition.showContactAdmin) { 215 providerInfo.setVisibility(View.VISIBLE); 216 } else { 217 providerInfo.setVisibility(View.INVISIBLE); 218 } 219 } 220 221 private TransitionScreenWrapper getTransitionForIndex(int currentTransitionIndex) { 222 TransitionScreenWrapper[] transitions = mProvisioningModeWrapper.transitions; 223 return transitions[currentTransitionIndex % transitions.length]; 224 } 225 226 @VisibleForTesting 227 ProvisioningModeWrapper getProvisioningModeWrapper( 228 @ProvisioningMode int provisioningMode) { 229 switch (provisioningMode) { 230 case PROVISIONING_MODE_WORK_PROFILE: 231 return WORK_PROFILE_WRAPPER; 232 case PROVISIONING_MODE_FULLY_MANAGED_DEVICE: 233 return FULLY_MANAGED_DEVICE_WRAPPER; 234 case PROVISIONING_MODE_WORK_PROFILE_ON_ORG_OWNED_DEVICE: 235 return WORK_PROFILE_ON_ORG_OWNED_DEVICE_WRAPPER; 236 } 237 throw new IllegalStateException("Unexpected provisioning mode " + provisioningMode); 238 } 239 240 private boolean shouldShowAnimations() { 241 final Context context = mAnimationComponents.header.getContext(); 242 return context.getResources().getBoolean(R.bool.show_edu_animations); 243 } 244 245 private void applyContentDescription() { 246 final TextView header = mAnimationComponents.header; 247 final Context context = header.getContext(); 248 header.setContentDescription(context.getString(mProvisioningModeWrapper.summary)); 249 } 250 251 private static final class TransitionScreenWrapper { 252 final @StringRes int header; 253 final @DrawableRes int drawable; 254 final @StringRes int subHeader; 255 final boolean showContactAdmin; 256 final boolean shouldLoop; 257 258 TransitionScreenWrapper(@StringRes int header, @DrawableRes int drawable) { 259 this(header, drawable, /* subHeader */ 0, /* showContactAdmin */ false, 260 /* shouldLoop */ true); 261 } 262 263 TransitionScreenWrapper(@StringRes int header, @DrawableRes int drawable, 264 @StringRes int subHeader, boolean showContactAdmin, boolean shouldLoop) { 265 this.header = checkNotNull(header, 266 "Header resource id must be a positive number."); 267 this.drawable = checkNotNull(drawable, 268 "Drawable resource id must be a positive number."); 269 this.subHeader = subHeader; 270 this.showContactAdmin = showContactAdmin; 271 this.shouldLoop = shouldLoop; 272 } 273 } 274 275 private static final class ProvisioningModeWrapper { 276 final TransitionScreenWrapper[] transitions; 277 final @StringRes int summary; 278 279 ProvisioningModeWrapper(TransitionScreenWrapper[] transitions, @StringRes int summary) { 280 this.transitions = checkNotNull(transitions); 281 this.summary = summary; 282 } 283 } 284 285 static final class AnimationComponents { 286 private final TextView header; 287 private final TextView subHeader; 288 private final ImageView image; 289 private final TextView providerInfo; 290 291 AnimationComponents( 292 TextView header, TextView subHeader, ImageView image, TextView providerInfo) { 293 this.header = checkNotNull(header); 294 this.subHeader = checkNotNull(subHeader); 295 this.image = checkNotNull(image); 296 this.providerInfo = checkNotNull(providerInfo); 297 } 298 299 List<View> asList() { 300 return Arrays.asList(header, subHeader, image, providerInfo); 301 } 302 } 303 } 304