| /* |
| * Copyright 2019, The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.managedprovisioning.provisioning; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.annotation.StringRes; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.Space; |
| import android.widget.TextView; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.managedprovisioning.R; |
| import com.android.managedprovisioning.common.CrossFadeHelper; |
| import com.android.managedprovisioning.common.CrossFadeHelper.Callback; |
| import com.android.managedprovisioning.common.StylerHelper; |
| import com.android.managedprovisioning.provisioning.ProvisioningModeWrapperProvider.ProvisioningModeWrapper; |
| |
| import com.airbnb.lottie.LottieAnimationView; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * Handles the animated transitions in the education screens. Transitions consist of cross fade |
| * animations between different headers and banner images. |
| */ |
| class TransitionAnimationHelper { |
| |
| interface TransitionAnimationCallback { |
| void onAllTransitionsShown(); |
| |
| void onAnimationSetup(LottieAnimationView animationView); |
| |
| } |
| interface TransitionAnimationStateManager { |
| void saveState(TransitionAnimationState state); |
| |
| TransitionAnimationState restoreState(); |
| } |
| |
| private static final int TRANSITION_TIME_MILLIS = 5000; |
| private static final int CROSSFADE_ANIMATION_DURATION_MILLIS = 500; |
| |
| private final CrossFadeHelper mCrossFadeHelper; |
| private final AnimationComponents mAnimationComponents; |
| private final Runnable mStartNextTransitionRunnable = this::startNextAnimation; |
| private final boolean mShowAnimations; |
| private TransitionAnimationCallback mCallback; |
| private TransitionAnimationStateManager mStateManager; |
| private final ProvisioningModeWrapper mProvisioningModeWrapper; |
| |
| private Handler mUiThreadHandler = new Handler(Looper.getMainLooper()); |
| private TransitionAnimationState mTransitionAnimationState; |
| private final StylerHelper mStylerHelper; |
| |
| TransitionAnimationHelper(AnimationComponents animationComponents, |
| TransitionAnimationCallback callback, |
| TransitionAnimationStateManager stateManager, |
| StylerHelper stylerHelper, |
| ProvisioningModeWrapper provisioningModeWrapper) { |
| mAnimationComponents = requireNonNull(animationComponents); |
| mCallback = requireNonNull(callback); |
| mStateManager = requireNonNull(stateManager); |
| mProvisioningModeWrapper = provisioningModeWrapper; |
| mCrossFadeHelper = getCrossFadeHelper(); |
| mShowAnimations = shouldShowAnimations(); |
| mStylerHelper = requireNonNull(stylerHelper); |
| |
| applyContentDescription( |
| mAnimationComponents.mAnimationView, |
| mProvisioningModeWrapper.summary); |
| } |
| |
| boolean areAllTransitionsShown() { |
| return mTransitionAnimationState.mAnimationIndex |
| == mProvisioningModeWrapper.transitions.length - 1; |
| } |
| |
| void start() { |
| mTransitionAnimationState = maybeRestoreState(); |
| scheduleNextTransition(getTimeLeftForTransition(mTransitionAnimationState)); |
| updateUiValues(mTransitionAnimationState.mAnimationIndex); |
| startCurrentAnimatedDrawable(mTransitionAnimationState.mProgress); |
| } |
| |
| private long getTimeLeftForTransition(TransitionAnimationState transitionAnimationState) { |
| long timeSinceLastTransition = |
| System.currentTimeMillis() - transitionAnimationState.mLastTransitionTimestamp; |
| return TRANSITION_TIME_MILLIS - timeSinceLastTransition; |
| } |
| |
| void stop() { |
| updateState(); |
| mStateManager.saveState(mTransitionAnimationState); |
| clean(); |
| } |
| |
| private void updateState() { |
| mTransitionAnimationState.mProgress = mAnimationComponents.mAnimationView.getProgress(); |
| } |
| |
| private TransitionAnimationState maybeRestoreState() { |
| TransitionAnimationState transitionAnimationState = mStateManager.restoreState(); |
| if (transitionAnimationState == null) { |
| return new TransitionAnimationState( |
| /* animationIndex */ 0, |
| /* progress */ 0, |
| /* lastTransitionTimestamp */ System.currentTimeMillis()); |
| } |
| return transitionAnimationState; |
| } |
| |
| private void clean() { |
| stopCurrentAnimatedDrawable(); |
| mCrossFadeHelper.cleanup(); |
| mUiThreadHandler.removeCallbacksAndMessages(null); |
| mUiThreadHandler = null; |
| mCallback = null; |
| mStateManager = null; |
| } |
| |
| @VisibleForTesting |
| CrossFadeHelper getCrossFadeHelper() { |
| return new CrossFadeHelper( |
| mAnimationComponents.asList(), |
| CROSSFADE_ANIMATION_DURATION_MILLIS, |
| new Callback() { |
| @Override |
| public void fadeOutCompleted() { |
| stopCurrentAnimatedDrawable(); |
| mTransitionAnimationState.mAnimationIndex++; |
| updateUiValues(mTransitionAnimationState.mAnimationIndex); |
| startCurrentAnimatedDrawable(/* startProgress */ 0f); |
| } |
| |
| @Override |
| public void fadeInCompleted() { |
| mTransitionAnimationState.mLastTransitionTimestamp = System.currentTimeMillis(); |
| scheduleNextTransition(TRANSITION_TIME_MILLIS); |
| } |
| }); |
| } |
| |
| private void scheduleNextTransition(long timeLeftForTransition) { |
| mUiThreadHandler.postDelayed(mStartNextTransitionRunnable, timeLeftForTransition); |
| } |
| |
| @VisibleForTesting |
| void startNextAnimation() { |
| if (mTransitionAnimationState.mAnimationIndex |
| >= mProvisioningModeWrapper.transitions.length - 1) { |
| if (mCallback != null) { |
| mCallback.onAllTransitionsShown(); |
| } |
| return; |
| } |
| mCrossFadeHelper.start(); |
| } |
| |
| @VisibleForTesting |
| void startCurrentAnimatedDrawable(float startProgress) { |
| if (!mShowAnimations) { |
| return; |
| } |
| boolean shouldLoop = |
| getTransitionForIndex(mTransitionAnimationState.mAnimationIndex).shouldLoop; |
| mAnimationComponents.mAnimationView.loop(shouldLoop); |
| mAnimationComponents.mAnimationView.setProgress(startProgress); |
| mAnimationComponents.mAnimationView.playAnimation(); |
| } |
| |
| @VisibleForTesting |
| void stopCurrentAnimatedDrawable() { |
| if (!mShowAnimations) { |
| return; |
| } |
| mAnimationComponents.mAnimationView.pauseAnimation(); |
| } |
| |
| @VisibleForTesting |
| void updateUiValues(int currentTransitionIndex) { |
| final TransitionScreenWrapper transition = |
| getTransitionForIndex(currentTransitionIndex); |
| setupHeaderText(transition); |
| setupDescriptionText(transition); |
| setupAnimation(transition); |
| |
| boolean isTextBasedEduScreen = transition.subHeaderIcon != 0; |
| updateItemValues( |
| mAnimationComponents.mItem1, |
| transition.subHeaderIcon, |
| transition.subHeaderTitle, |
| transition.subHeader, |
| isTextBasedEduScreen); |
| updateItemValues( |
| mAnimationComponents.mItem2, |
| transition.secondarySubHeaderIcon, |
| transition.secondarySubHeaderTitle, |
| transition.secondarySubHeader, |
| isTextBasedEduScreen); |
| updateSpaceVisibility(mAnimationComponents.mSpace1, isTextBasedEduScreen); |
| updateSpaceVisibility(mAnimationComponents.mSpace2, isTextBasedEduScreen); |
| } |
| |
| private void setupHeaderText(TransitionScreenWrapper transition) { |
| mAnimationComponents.mHeader.setText(transition.header); |
| triggerTextToSpeechIfFocused(mAnimationComponents.mHeader); |
| } |
| |
| private void triggerTextToSpeechIfFocused(TextView view) { |
| if (view.isAccessibilityFocused()) { |
| view.announceForAccessibility(view.getText().toString()); |
| } |
| } |
| |
| private void setupAnimation(TransitionScreenWrapper transition) { |
| if (mShowAnimations && transition.drawable != 0) { |
| mAnimationComponents.mAnimationView.setAnimation(transition.drawable); |
| mCallback.onAnimationSetup(mAnimationComponents.mAnimationView); |
| mAnimationComponents.mImageContainer.setVisibility(View.VISIBLE); |
| } else { |
| mAnimationComponents.mImageContainer.setVisibility(View.GONE); |
| } |
| } |
| |
| private void setupDescriptionText(TransitionScreenWrapper transition) { |
| if (transition.description != 0) { |
| mAnimationComponents.mDescription.setText(transition.description); |
| mAnimationComponents.mDescription.setVisibility(View.VISIBLE); |
| triggerTextToSpeechIfFocused(mAnimationComponents.mDescription); |
| } else { |
| mAnimationComponents.mDescription.setVisibility(View.GONE); |
| } |
| } |
| |
| private void updateItemValues(ViewGroup item, int icon, int subHeaderTitle, int subHeader, |
| boolean isTextBasedEduScreen) { |
| if (isTextBasedEduScreen) { |
| ((ImageView) item.findViewById(R.id.sud_items_icon)).setImageResource(icon); |
| ((TextView) item.findViewById(R.id.sud_items_title)).setText(subHeaderTitle); |
| ((TextView) item.findViewById(R.id.sud_items_summary)).setText(subHeader); |
| mStylerHelper.applyListItemStyling( |
| item, new LinearLayout.LayoutParams(item.getLayoutParams())); |
| item.setVisibility(View.VISIBLE); |
| } else { |
| item.setVisibility(View.GONE); |
| } |
| } |
| |
| private void updateSpaceVisibility(Space space, boolean isTextBasedEduScreen) { |
| if (isTextBasedEduScreen) { |
| space.setVisibility(View.VISIBLE); |
| } else { |
| space.setVisibility(View.GONE); |
| } |
| } |
| |
| private TransitionScreenWrapper getTransitionForIndex(int currentTransitionIndex) { |
| TransitionScreenWrapper[] transitions = mProvisioningModeWrapper.transitions; |
| return transitions[currentTransitionIndex % transitions.length]; |
| } |
| |
| private boolean shouldShowAnimations() { |
| final Context context = mAnimationComponents.mHeader.getContext(); |
| return context.getResources().getBoolean(R.bool.show_edu_animations); |
| } |
| |
| private void applyContentDescription(View view, @StringRes int summaryRes) { |
| Context context = view.getContext(); |
| view.setContentDescription(context.getString(summaryRes)); |
| } |
| |
| static final class AnimationComponents { |
| private final TextView mHeader; |
| private final TextView mDescription; |
| private final LottieAnimationView mAnimationView; |
| private final ViewGroup mImageContainer; |
| private final ViewGroup mItem1; |
| private final ViewGroup mItem2; |
| private final Space mSpace1; |
| private final Space mSpace2; |
| |
| AnimationComponents(TextView header, TextView description, ViewGroup item1, |
| ViewGroup item2, LottieAnimationView animationView, ViewGroup imageContainer, |
| Space space1, Space space2) { |
| this.mHeader = requireNonNull(header); |
| this.mDescription = requireNonNull(description); |
| this.mItem1 = requireNonNull(item1); |
| this.mItem2 = requireNonNull(item2); |
| this.mImageContainer = requireNonNull(imageContainer); |
| this.mAnimationView = requireNonNull(animationView); |
| this.mSpace1 = requireNonNull(space1); |
| this.mSpace2 = requireNonNull(space2); |
| } |
| |
| List<View> asList() { |
| return Arrays.asList(mHeader, mItem1, mItem2, mImageContainer); |
| } |
| } |
| |
| static final class TransitionAnimationState { |
| private int mAnimationIndex; |
| private float mProgress; |
| private long mLastTransitionTimestamp; |
| |
| TransitionAnimationState( |
| int animationIndex, |
| float progress, |
| long lastTransitionTimestamp) { |
| mAnimationIndex = animationIndex; |
| mProgress = progress; |
| mLastTransitionTimestamp = lastTransitionTimestamp; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (!(o instanceof TransitionAnimationState)) return false; |
| TransitionAnimationState that = (TransitionAnimationState) o; |
| return mAnimationIndex == that.mAnimationIndex && |
| Float.compare(that.mProgress, mProgress) == 0 && |
| mLastTransitionTimestamp == that.mLastTransitionTimestamp; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mAnimationIndex, mProgress, mLastTransitionTimestamp); |
| } |
| } |
| } |