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