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 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