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