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 
17 package com.android.messaging.ui;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.media.MediaPlayer;
22 import android.net.Uri;
23 import android.util.AttributeSet;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.FrameLayout;
28 import android.widget.ImageButton;
29 import android.widget.ImageView.ScaleType;
30 import android.widget.VideoView;
31 
32 import com.android.messaging.R;
33 import com.android.messaging.datamodel.data.MessagePartData;
34 import com.android.messaging.datamodel.media.ImageRequest;
35 import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor;
36 import com.android.messaging.datamodel.media.VideoThumbnailRequest;
37 import 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  */
45 public 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 
VideoThumbnailView(final Context context, final AttributeSet attrs)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
onAnimationStart()172     protected void onAnimationStart() {
173         super.onAnimationStart();
174         mAnimating = true;
175     }
176 
177     @Override
onAnimationEnd()178     protected void onAnimationEnd() {
179         super.onAnimationEnd();
180         mAnimating = false;
181         trySwitchToVideo();
182     }
183 
trySwitchToVideo()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 
hasVideoSize()202     private boolean hasVideoSize() {
203         return mVideoWidth != ImageRequest.UNSPECIFIED_SIZE &&
204                 mVideoHeight != ImageRequest.UNSPECIFIED_SIZE;
205     }
206 
start()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.
shouldUseGenericVideoIcon(final boolean incomingMessage)216     private static boolean shouldUseGenericVideoIcon(final boolean incomingMessage) {
217         return incomingMessage && !VideoThumbnailRequest.shouldShowIncomingVideoThumbnails();
218     }
219 
setSource(final MessagePartData part, final boolean incomingMessage)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 
setSource(final Uri videoSource, final boolean incomingMessage)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 
clearSource()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
setMinimumWidth(final int minWidth)271     public void setMinimumWidth(final int minWidth) {
272         super.setMinimumWidth(minWidth);
273         if (mVideoView != null) {
274             mVideoView.setMinimumWidth(minWidth);
275         }
276     }
277 
278     @Override
setMinimumHeight(final int minHeight)279     public void setMinimumHeight(final int minHeight) {
280         super.setMinimumHeight(minHeight);
281         if (mVideoView != null) {
282             mVideoView.setMinimumHeight(minHeight);
283         }
284     }
285 
setColorFilter(int color)286     public void setColorFilter(int color) {
287         mThumbnailImage.setColorFilter(color);
288         mPlayButton.setColorFilter(color);
289     }
290 
clearColorFilter()291     public void clearColorFilter() {
292         mThumbnailImage.clearColorFilter();
293         mPlayButton.clearColorFilter();
294     }
295 
296     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)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
onLayout(final boolean changed, final int left, final int top, final int right, final int bottom)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