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