Home | History | Annotate | Download | only in ui
      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