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