blob: 19a90472b8775726f65e861aff23e8470c993887 [file]
/*
* 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);
}
}
}