1/*
2 * Copyright 2016, 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 */
16package com.android.managedprovisioning.preprovisioning.anim;
17
18import static com.android.internal.util.Preconditions.checkNotNull;
19
20import android.animation.Animator;
21import android.animation.AnimatorInflater;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.app.Activity;
25import android.graphics.drawable.Animatable2;
26import android.graphics.drawable.AnimatedVectorDrawable;
27import android.graphics.drawable.Drawable;
28import android.support.annotation.NonNull;
29import android.view.ContextThemeWrapper;
30import android.view.View;
31import android.view.ViewGroup.LayoutParams;
32import android.widget.ImageView;
33import android.widget.TextView;
34
35import com.android.managedprovisioning.R;
36import com.android.managedprovisioning.model.CustomizationParams;
37
38import java.util.List;
39
40/**
41 * <p>Drives the animation showing benefits of having a Managed Profile.
42 * <p>Tightly coupled with the {@link R.layout#intro_animation} layout.
43 */
44public class BenefitsAnimation {
45    /** Array of Id pairs: {{@link ObjectAnimator}, {@link TextView}} */
46    private static final int[][] ID_ANIMATION_TARGET = {
47            {R.anim.text_scene_0_animation, R.id.text_0},
48            {R.anim.text_scene_1_animation, R.id.text_1},
49            {R.anim.text_scene_2_animation, R.id.text_2},
50            {R.anim.text_scene_3_animation, R.id.text_3},
51            {R.anim.text_scene_master_animation, R.id.text_master}};
52
53    private static final int[] SLIDE_CAPTION_TEXT_VIEWS = {
54            R.id.text_0, R.id.text_1, R.id.text_2, R.id.text_3};
55
56    /** Id of an {@link ImageView} containing the animated graphic */
57    private static final int ID_ANIMATED_GRAPHIC = R.id.animated_info;
58
59    private static final int SLIDE_COUNT = 3;
60    private static final int ANIMATION_ORIGINAL_WIDTH_PX = 1080;
61
62    private final AnimatedVectorDrawable mTopAnimation;
63    private final Animator mTextAnimation;
64    private final Activity mActivity;
65
66    private boolean mStopped;
67
68    /**
69     * @param captions slide captions for the animation
70     * @param contentDescription for accessibility
71     */
72    public BenefitsAnimation(@NonNull Activity activity, @NonNull List<Integer> captions,
73            int contentDescription, CustomizationParams customizationParams) {
74        if (captions.size() != SLIDE_COUNT) {
75            throw new IllegalArgumentException(
76                    "Wrong number of slide captions. Expected: " + SLIDE_COUNT);
77        }
78        mActivity = checkNotNull(activity);
79        mTextAnimation = checkNotNull(assembleTextAnimation());
80        applySlideCaptions(captions);
81        applyContentDescription(contentDescription);
82
83        setTopInfoDrawable(customizationParams);
84
85        mTopAnimation = checkNotNull(extractAnimationFromImageView(ID_ANIMATED_GRAPHIC));
86
87        // chain all animations together
88        chainAnimations();
89
90        // once the screen is ready, adjust size
91        mActivity.findViewById(android.R.id.content).post(this::adjustToScreenSize);
92    }
93
94    private void setTopInfoDrawable(CustomizationParams customizationParams) {
95        int swiperTheme = new SwiperThemeMatcher(mActivity, new ColorMatcher())
96                .findTheme(customizationParams.mainColor);
97
98        ContextThemeWrapper wrapper = new ContextThemeWrapper(mActivity, swiperTheme);
99        Drawable drawable =
100                mActivity.getResources().getDrawable(
101                        R.drawable.topinfo_animation,
102                        wrapper.getTheme());
103        ImageView imageView = mActivity.findViewById(ID_ANIMATED_GRAPHIC);
104        imageView.setImageDrawable(drawable);
105    }
106
107    /** Starts playing the animation in a loop. */
108    public void start() {
109        mStopped = false;
110        mTopAnimation.start();
111    }
112
113    /** Stops the animation. */
114    public void stop() {
115        mStopped = true;
116        mTopAnimation.stop();
117    }
118
119    /**
120     * Adjust animation and text to match actual screen size
121     */
122    private void adjustToScreenSize() {
123        if (mActivity.isDestroyed()) {
124            return;
125        }
126
127        ImageView animatedInfo = mActivity.findViewById(R.id.animated_info);
128        int widthPx = animatedInfo.getWidth();
129        float scaleRatio = (float) widthPx / ANIMATION_ORIGINAL_WIDTH_PX;
130
131        // adjust animation height; width happens automatically
132        LayoutParams layoutParams = animatedInfo.getLayoutParams();
133        int originalHeight = animatedInfo.getHeight();
134        int adjustedHeight = (int) (originalHeight * scaleRatio);
135        layoutParams.height = adjustedHeight;
136        animatedInfo.setLayoutParams(layoutParams);
137
138        // adjust captions size only if downscaling
139        if (scaleRatio < 1) {
140            for (int textViewId : SLIDE_CAPTION_TEXT_VIEWS) {
141                View view = mActivity.findViewById(textViewId);
142                view.setScaleX(scaleRatio);
143                view.setScaleY(scaleRatio);
144            }
145        }
146
147        // if the content is bigger than the screen, try to shrink just the animation
148        int offset = adjustedHeight - originalHeight;
149        int contentHeight = mActivity.findViewById(R.id.intro_po_content).getHeight() + offset;
150        int viewportHeight = mActivity.findViewById(R.id.suw_layout_content).getHeight();
151        if (contentHeight > viewportHeight) {
152            int targetHeight = layoutParams.height - (contentHeight - viewportHeight);
153            int minHeight = mActivity.getResources().getDimensionPixelSize(
154                    R.dimen.intro_animation_min_height);
155
156            // if the animation becomes too small, leave it as is and the scrollbar will show
157            if (targetHeight >= minHeight) {
158                layoutParams.height = targetHeight;
159                animatedInfo.setLayoutParams(layoutParams);
160            }
161        }
162    }
163
164    /**
165     * <p>Chains all three sub-animations, and configures them to play in sync in a loop.
166     * <p>Looping {@link AnimatedVectorDrawable} and {@link AnimatorSet} currently not possible in
167     * XML.
168     */
169    private void chainAnimations() {
170        mTopAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
171            @Override
172            public void onAnimationStart(Drawable drawable) {
173                super.onAnimationStart(drawable);
174
175                // starting the other animations at the same time
176                mTextAnimation.start();
177            }
178
179            @Override
180            public void onAnimationEnd(Drawable drawable) {
181                super.onAnimationEnd(drawable);
182
183                // without explicitly stopping them, sometimes they won't restart
184                mTextAnimation.cancel();
185
186                // repeating the animation in loop
187                if (!mStopped) {
188                    mTopAnimation.start();
189                }
190            }
191        });
192    }
193
194    /**
195     * <p>Inflates animators required to animate text headers' part of the whole animation.
196     * <p>This has to be done through code, as setting a target on {@link
197     * android.animation.ObjectAnimator} is not currently possible in XML.
198     *
199     * @return {@link AnimatorSet} responsible for the animated text
200     */
201    private AnimatorSet assembleTextAnimation() {
202        Animator[] animators = new Animator[ID_ANIMATION_TARGET.length];
203        for (int i = 0; i < ID_ANIMATION_TARGET.length; i++) {
204            int[] instance = ID_ANIMATION_TARGET[i];
205            animators[i] = AnimatorInflater.loadAnimator(mActivity, instance[0]);
206            animators[i].setTarget(mActivity.findViewById(instance[1]));
207        }
208
209        AnimatorSet animatorSet = new AnimatorSet();
210        animatorSet.playTogether(animators);
211        return animatorSet;
212    }
213
214    /**
215     * @param captions slide titles
216     */
217    private void applySlideCaptions(List<Integer> captions) {
218        int slideIx = 0;
219        for (int viewId : SLIDE_CAPTION_TEXT_VIEWS) {
220            ((TextView) mActivity.findViewById(viewId)).setText(
221                    captions.get(slideIx++ % captions.size()));
222        }
223    }
224
225    private void applyContentDescription(int contentDescription) {
226        mActivity.findViewById(R.id.animation_top_level_frame).setContentDescription(
227                mActivity.getString(contentDescription));
228    }
229
230    /** Extracts an {@link AnimatedVectorDrawable} from a containing {@link ImageView}. */
231    private AnimatedVectorDrawable extractAnimationFromImageView(int id) {
232        ImageView imageView = mActivity.findViewById(id);
233        return (AnimatedVectorDrawable) imageView.getDrawable();
234    }
235}