1/*
2 * Copyright (C) 2015 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
17package com.android.messaging.ui;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.media.MediaPlayer;
22import android.net.Uri;
23import android.util.AttributeSet;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.FrameLayout;
28import android.widget.ImageButton;
29import android.widget.ImageView.ScaleType;
30import android.widget.VideoView;
31
32import com.android.messaging.R;
33import com.android.messaging.datamodel.data.MessagePartData;
34import com.android.messaging.datamodel.media.ImageRequest;
35import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor;
36import com.android.messaging.datamodel.media.VideoThumbnailRequest;
37import com.android.messaging.util.Assert;
38
39/**
40 * View that encapsulates a video preview (either as a thumbnail image, or video player), and the
41 * a play button to overlay it.  Ensures that the video preview maintains the aspect ratio of the
42 * original video while trying to respect minimum width/height and constraining to the available
43 * bounds
44 */
45public class VideoThumbnailView extends FrameLayout {
46    /**
47     * When in this mode the VideoThumbnailView is a lightweight AsyncImageView with an ImageButton
48     * to play the video.  Clicking play will launch a full screen player
49     */
50    private static final int MODE_IMAGE_THUMBNAIL = 0;
51
52    /**
53     * When in this mode the VideoThumbnailVideo will include a VideoView, and the play button will
54     * play the video inline.  When in this mode, the loop and playOnLoad attributes can be applied
55     * to auto-play or loop the video.
56     */
57    private static final int MODE_PLAYABLE_VIDEO = 1;
58
59    private final int mMode;
60    private final boolean mPlayOnLoad;
61    private final boolean mAllowCrop;
62    private final VideoView mVideoView;
63    private final ImageButton mPlayButton;
64    private final AsyncImageView mThumbnailImage;
65    private int mVideoWidth;
66    private int mVideoHeight;
67    private Uri mVideoSource;
68    private boolean mAnimating;
69    private boolean mVideoLoaded;
70
71    public VideoThumbnailView(final Context context, final AttributeSet attrs) {
72        super(context, attrs);
73        final TypedArray typedAttributes =
74                context.obtainStyledAttributes(attrs, R.styleable.VideoThumbnailView);
75
76        final LayoutInflater inflater = LayoutInflater.from(context);
77        inflater.inflate(R.layout.video_thumbnail_view, this, true);
78
79        mPlayOnLoad = typedAttributes.getBoolean(R.styleable.VideoThumbnailView_playOnLoad, false);
80        final boolean loop =
81                typedAttributes.getBoolean(R.styleable.VideoThumbnailView_loop, false);
82        mMode = typedAttributes.getInt(R.styleable.VideoThumbnailView_mode, MODE_IMAGE_THUMBNAIL);
83        mAllowCrop = typedAttributes.getBoolean(R.styleable.VideoThumbnailView_allowCrop, false);
84
85        mVideoWidth = ImageRequest.UNSPECIFIED_SIZE;
86        mVideoHeight = ImageRequest.UNSPECIFIED_SIZE;
87
88        if (mMode == MODE_PLAYABLE_VIDEO) {
89            mVideoView = new VideoView(context);
90            // Video view tries to request focus on start which pulls focus from the user's intended
91            // focus when we add this control.  Remove focusability to prevent this.  The play
92            // button can still be focused
93            mVideoView.setFocusable(false);
94            mVideoView.setFocusableInTouchMode(false);
95            mVideoView.clearFocus();
96            addView(mVideoView, 0, new ViewGroup.LayoutParams(
97                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
98            mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
99                @Override
100                public void onPrepared(final MediaPlayer mediaPlayer) {
101                    mVideoLoaded = true;
102                    mVideoWidth = mediaPlayer.getVideoWidth();
103                    mVideoHeight = mediaPlayer.getVideoHeight();
104                    mediaPlayer.setLooping(loop);
105                    trySwitchToVideo();
106                }
107            });
108            mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
109                @Override
110                public void onCompletion(final MediaPlayer mediaPlayer) {
111                    mPlayButton.setVisibility(View.VISIBLE);
112                }
113            });
114            mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
115                @Override
116                public boolean onError(final MediaPlayer mediaPlayer, final int i, final int i2) {
117                    return true;
118                }
119            });
120        } else {
121            mVideoView = null;
122        }
123
124        mPlayButton = (ImageButton) findViewById(R.id.video_thumbnail_play_button);
125        if (loop) {
126            mPlayButton.setVisibility(View.GONE);
127        } else {
128            mPlayButton.setOnClickListener(new OnClickListener() {
129                @Override
130                public void onClick(final View view) {
131                    if (mVideoSource == null) {
132                        return;
133                    }
134
135                    if (mMode == MODE_PLAYABLE_VIDEO) {
136                        mVideoView.seekTo(0);
137                        start();
138                    } else {
139                        UIIntents.get().launchFullScreenVideoViewer(getContext(), mVideoSource);
140                    }
141                }
142            });
143            mPlayButton.setOnLongClickListener(new OnLongClickListener() {
144                @Override
145                public boolean onLongClick(final View view) {
146                    // Button prevents long click from propagating up, do it manually
147                    VideoThumbnailView.this.performLongClick();
148                    return true;
149                }
150            });
151        }
152
153        mThumbnailImage = (AsyncImageView) findViewById(R.id.video_thumbnail_image);
154        if (mAllowCrop) {
155            mThumbnailImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
156            mThumbnailImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
157            mThumbnailImage.setScaleType(ScaleType.CENTER_CROP);
158        } else {
159            // This is the default setting in the layout, so No-op.
160        }
161        final int maxHeight = typedAttributes.getDimensionPixelSize(
162                R.styleable.VideoThumbnailView_android_maxHeight, ImageRequest.UNSPECIFIED_SIZE);
163        if (maxHeight != ImageRequest.UNSPECIFIED_SIZE) {
164            mThumbnailImage.setMaxHeight(maxHeight);
165            mThumbnailImage.setAdjustViewBounds(true);
166        }
167
168        typedAttributes.recycle();
169    }
170
171    @Override
172    protected void onAnimationStart() {
173        super.onAnimationStart();
174        mAnimating = true;
175    }
176
177    @Override
178    protected void onAnimationEnd() {
179        super.onAnimationEnd();
180        mAnimating = false;
181        trySwitchToVideo();
182    }
183
184    private void trySwitchToVideo() {
185        if (mAnimating) {
186            // Don't start video or hide image until after animation completes
187            return;
188        }
189
190        if (!mVideoLoaded) {
191            // Video hasn't loaded, nothing more to do
192            return;
193        }
194
195        if (mPlayOnLoad) {
196            start();
197        } else {
198            mVideoView.seekTo(0);
199        }
200    }
201
202    private boolean hasVideoSize() {
203        return mVideoWidth != ImageRequest.UNSPECIFIED_SIZE &&
204                mVideoHeight != ImageRequest.UNSPECIFIED_SIZE;
205    }
206
207    public void start() {
208        Assert.equals(MODE_PLAYABLE_VIDEO, mMode);
209        mPlayButton.setVisibility(View.GONE);
210        mThumbnailImage.setVisibility(View.GONE);
211        mVideoView.start();
212    }
213
214    // TODO: The check could be added to MessagePartData itself so that all users of MessagePartData
215    // get the right behavior, instead of requiring all the users to do similar checks.
216    private static boolean shouldUseGenericVideoIcon(final boolean incomingMessage) {
217        return incomingMessage && !VideoThumbnailRequest.shouldShowIncomingVideoThumbnails();
218    }
219
220    public void setSource(final MessagePartData part, final boolean incomingMessage) {
221        if (part == null) {
222            clearSource();
223        } else {
224            mVideoSource = part.getContentUri();
225            if (shouldUseGenericVideoIcon(incomingMessage)) {
226                mThumbnailImage.setImageResource(R.drawable.generic_video_icon);
227                mVideoWidth = ImageRequest.UNSPECIFIED_SIZE;
228                mVideoHeight = ImageRequest.UNSPECIFIED_SIZE;
229            } else {
230                mThumbnailImage.setImageResourceId(
231                        new MessagePartVideoThumbnailRequestDescriptor(part));
232                if (mVideoView != null) {
233                    mVideoView.setVideoURI(mVideoSource);
234                }
235                mVideoWidth = part.getWidth();
236                mVideoHeight = part.getHeight();
237            }
238        }
239    }
240
241    public void setSource(final Uri videoSource, final boolean incomingMessage) {
242        if (videoSource == null) {
243            clearSource();
244        } else {
245            mVideoSource = videoSource;
246            if (shouldUseGenericVideoIcon(incomingMessage)) {
247                mThumbnailImage.setImageResource(R.drawable.generic_video_icon);
248                mVideoWidth = ImageRequest.UNSPECIFIED_SIZE;
249                mVideoHeight = ImageRequest.UNSPECIFIED_SIZE;
250            } else {
251                mThumbnailImage.setImageResourceId(
252                        new MessagePartVideoThumbnailRequestDescriptor(videoSource));
253                if (mVideoView != null) {
254                    mVideoView.setVideoURI(videoSource);
255                }
256            }
257        }
258    }
259
260    private void clearSource() {
261        mVideoSource = null;
262        mThumbnailImage.setImageResourceId(null);
263        mVideoWidth = ImageRequest.UNSPECIFIED_SIZE;
264        mVideoHeight = ImageRequest.UNSPECIFIED_SIZE;
265        if (mVideoView != null) {
266            mVideoView.setVideoURI(null);
267        }
268    }
269
270    @Override
271    public void setMinimumWidth(final int minWidth) {
272        super.setMinimumWidth(minWidth);
273        if (mVideoView != null) {
274            mVideoView.setMinimumWidth(minWidth);
275        }
276    }
277
278    @Override
279    public void setMinimumHeight(final int minHeight) {
280        super.setMinimumHeight(minHeight);
281        if (mVideoView != null) {
282            mVideoView.setMinimumHeight(minHeight);
283        }
284    }
285
286    public void setColorFilter(int color) {
287        mThumbnailImage.setColorFilter(color);
288        mPlayButton.setColorFilter(color);
289    }
290
291    public void clearColorFilter() {
292        mThumbnailImage.clearColorFilter();
293        mPlayButton.clearColorFilter();
294    }
295
296    @Override
297    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
298        if (mAllowCrop) {
299            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
300            return;
301        }
302        int desiredWidth = 1;
303        int desiredHeight = 1;
304        if (mVideoView != null) {
305            mVideoView.measure(widthMeasureSpec, heightMeasureSpec);
306        }
307        mThumbnailImage.measure(widthMeasureSpec, heightMeasureSpec);
308        if (hasVideoSize()) {
309            desiredWidth = mVideoWidth;
310            desiredHeight = mVideoHeight;
311        } else {
312            desiredWidth = mThumbnailImage.getMeasuredWidth();
313            desiredHeight = mThumbnailImage.getMeasuredHeight();
314        }
315
316        final int minimumWidth = getMinimumWidth();
317        final int minimumHeight = getMinimumHeight();
318
319        // Constrain the scale to fit within the supplied size
320        final float maxScale = Math.max(
321                MeasureSpec.getSize(widthMeasureSpec) / (float) desiredWidth,
322                MeasureSpec.getSize(heightMeasureSpec) / (float) desiredHeight);
323
324        // Scale up to reach minimum width/height
325        final float widthScale = Math.max(1, minimumWidth / (float) desiredWidth);
326        final float heightScale = Math.max(1, minimumHeight / (float) desiredHeight);
327        final float scale = Math.min(maxScale, Math.max(widthScale, heightScale));
328        desiredWidth = (int) (desiredWidth * scale);
329        desiredHeight = (int) (desiredHeight * scale);
330
331        setMeasuredDimension(desiredWidth, desiredHeight);
332    }
333
334    @Override
335    protected void onLayout(final boolean changed, final int left, final int top, final int right,
336            final int bottom) {
337        final int count = getChildCount();
338        for (int i = 0; i < count; i++) {
339            final View child = getChildAt(i);
340            child.layout(0, 0, right - left, bottom - top);
341        }
342    }
343}
344