Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2017 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.example.android.pictureinpicture.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.AssetFileDescriptor;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Color;
     23 import android.media.MediaPlayer;
     24 import android.os.Handler;
     25 import android.os.Message;
     26 import android.support.annotation.Nullable;
     27 import android.support.annotation.RawRes;
     28 import android.transition.TransitionManager;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.view.Surface;
     32 import android.view.SurfaceHolder;
     33 import android.view.SurfaceView;
     34 import android.view.View;
     35 import android.widget.ImageButton;
     36 import android.widget.RelativeLayout;
     37 
     38 import com.example.android.pictureinpicture.R;
     39 
     40 import java.io.IOException;
     41 import java.lang.ref.WeakReference;
     42 
     43 /**
     44  * Provides video playback. There is nothing directly related to Picture-in-Picture here.
     45  *
     46  * <p>This is similar to {@link android.widget.VideoView}, but it comes with a custom control
     47  * (play/pause, fast forward, and fast rewind).
     48  */
     49 public class MovieView extends RelativeLayout {
     50 
     51     /** Monitors all events related to {@link MovieView}. */
     52     public abstract static class MovieListener {
     53 
     54         /** Called when the video is started or resumed. */
     55         public void onMovieStarted() {}
     56 
     57         /** Called when the video is paused or finished. */
     58         public void onMovieStopped() {}
     59 
     60         /** Called when this view should be minimized. */
     61         public void onMovieMinimized() {}
     62     }
     63 
     64     private static final String TAG = "MovieView";
     65 
     66     /** The amount of time we are stepping forward or backward for fast-forward and fast-rewind. */
     67     private static final int FAST_FORWARD_REWIND_INTERVAL = 5000; // ms
     68 
     69     /** The amount of time until we fade out the controls. */
     70     private static final int TIMEOUT_CONTROLS = 3000; // ms
     71 
     72     /** Shows the video playback. */
     73     private final SurfaceView mSurfaceView;
     74 
     75     // Controls
     76     private final ImageButton mToggle;
     77     private final View mShade;
     78     private final ImageButton mFastForward;
     79     private final ImageButton mFastRewind;
     80     private final ImageButton mMinimize;
     81 
     82     /** This plays the video. This will be null when no video is set. */
     83     MediaPlayer mMediaPlayer;
     84 
     85     /** The resource ID for the video to play. */
     86     @RawRes private int mVideoResourceId;
     87 
     88     /** Whether we adjust our view bounds or we fill the remaining area with black bars */
     89     private boolean mAdjustViewBounds;
     90 
     91     /** Handles timeout for media controls. */
     92     TimeoutHandler mTimeoutHandler;
     93 
     94     /** The listener for all the events we publish. */
     95     MovieListener mMovieListener;
     96 
     97     private int mSavedCurrentPosition;
     98 
     99     public MovieView(Context context) {
    100         this(context, null);
    101     }
    102 
    103     public MovieView(Context context, AttributeSet attrs) {
    104         this(context, attrs, 0);
    105     }
    106 
    107     public MovieView(Context context, AttributeSet attrs, int defStyleAttr) {
    108         super(context, attrs, defStyleAttr);
    109         setBackgroundColor(Color.BLACK);
    110 
    111         // Inflate the content
    112         inflate(context, R.layout.view_movie, this);
    113         mSurfaceView = findViewById(R.id.surface);
    114         mShade = findViewById(R.id.shade);
    115         mToggle = findViewById(R.id.toggle);
    116         mFastForward = findViewById(R.id.fast_forward);
    117         mFastRewind = findViewById(R.id.fast_rewind);
    118         mMinimize = findViewById(R.id.minimize);
    119 
    120         // Attributes
    121         final TypedArray a =
    122                 context.obtainStyledAttributes(
    123                         attrs,
    124                         R.styleable.MovieView,
    125                         defStyleAttr,
    126                         R.style.Widget_PictureInPicture_MovieView);
    127         setVideoResourceId(a.getResourceId(R.styleable.MovieView_android_src, 0));
    128         setAdjustViewBounds(a.getBoolean(R.styleable.MovieView_android_adjustViewBounds, false));
    129         a.recycle();
    130 
    131         // Bind view events
    132         final OnClickListener listener =
    133                 new OnClickListener() {
    134                     @Override
    135                     public void onClick(View view) {
    136                         switch (view.getId()) {
    137                             case R.id.surface:
    138                                 toggleControls();
    139                                 break;
    140                             case R.id.toggle:
    141                                 toggle();
    142                                 break;
    143                             case R.id.fast_forward:
    144                                 fastForward();
    145                                 break;
    146                             case R.id.fast_rewind:
    147                                 fastRewind();
    148                                 break;
    149                             case R.id.minimize:
    150                                 if (mMovieListener != null) {
    151                                     mMovieListener.onMovieMinimized();
    152                                 }
    153                                 break;
    154                         }
    155                         // Start or reset the timeout to hide controls
    156                         if (mMediaPlayer != null) {
    157                             if (mTimeoutHandler == null) {
    158                                 mTimeoutHandler = new TimeoutHandler(MovieView.this);
    159                             }
    160                             mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS);
    161                             if (mMediaPlayer.isPlaying()) {
    162                                 mTimeoutHandler.sendEmptyMessageDelayed(
    163                                         TimeoutHandler.MESSAGE_HIDE_CONTROLS, TIMEOUT_CONTROLS);
    164                             }
    165                         }
    166                     }
    167                 };
    168         mSurfaceView.setOnClickListener(listener);
    169         mToggle.setOnClickListener(listener);
    170         mFastForward.setOnClickListener(listener);
    171         mFastRewind.setOnClickListener(listener);
    172         mMinimize.setOnClickListener(listener);
    173 
    174         // Prepare video playback
    175         mSurfaceView
    176                 .getHolder()
    177                 .addCallback(
    178                         new SurfaceHolder.Callback() {
    179                             @Override
    180                             public void surfaceCreated(SurfaceHolder holder) {
    181                                 openVideo(holder.getSurface());
    182                             }
    183 
    184                             @Override
    185                             public void surfaceChanged(
    186                                     SurfaceHolder holder, int format, int width, int height) {
    187                                 // Do nothing
    188                             }
    189 
    190                             @Override
    191                             public void surfaceDestroyed(SurfaceHolder holder) {
    192                                 if (mMediaPlayer != null) {
    193                                     mSavedCurrentPosition = mMediaPlayer.getCurrentPosition();
    194                                 }
    195                                 closeVideo();
    196                             }
    197                         });
    198     }
    199 
    200     @Override
    201     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    202         if (mMediaPlayer != null) {
    203             final int videoWidth = mMediaPlayer.getVideoWidth();
    204             final int videoHeight = mMediaPlayer.getVideoHeight();
    205             if (videoWidth != 0 && videoHeight != 0) {
    206                 final float aspectRatio = (float) videoHeight / videoWidth;
    207                 final int width = MeasureSpec.getSize(widthMeasureSpec);
    208                 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    209                 final int height = MeasureSpec.getSize(heightMeasureSpec);
    210                 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    211                 if (mAdjustViewBounds) {
    212                     if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
    213                         super.onMeasure(
    214                                 widthMeasureSpec,
    215                                 MeasureSpec.makeMeasureSpec(
    216                                         (int) (width * aspectRatio), MeasureSpec.EXACTLY));
    217                     } else if (widthMode != MeasureSpec.EXACTLY
    218                             && heightMode == MeasureSpec.EXACTLY) {
    219                         super.onMeasure(
    220                                 MeasureSpec.makeMeasureSpec(
    221                                         (int) (height / aspectRatio), MeasureSpec.EXACTLY),
    222                                 heightMeasureSpec);
    223                     } else {
    224                         super.onMeasure(
    225                                 widthMeasureSpec,
    226                                 MeasureSpec.makeMeasureSpec(
    227                                         (int) (width * aspectRatio), MeasureSpec.EXACTLY));
    228                     }
    229                 } else {
    230                     final float viewRatio = (float) height / width;
    231                     if (aspectRatio > viewRatio) {
    232                         int padding = (int) ((width - height / aspectRatio) / 2);
    233                         setPadding(padding, 0, padding, 0);
    234                     } else {
    235                         int padding = (int) ((height - width * aspectRatio) / 2);
    236                         setPadding(0, padding, 0, padding);
    237                     }
    238                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    239                 }
    240                 return;
    241             }
    242         }
    243         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    244     }
    245 
    246     @Override
    247     protected void onDetachedFromWindow() {
    248         if (mTimeoutHandler != null) {
    249             mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS);
    250             mTimeoutHandler = null;
    251         }
    252         super.onDetachedFromWindow();
    253     }
    254 
    255     /**
    256      * Sets the listener to monitor movie events.
    257      *
    258      * @param movieListener The listener to be set.
    259      */
    260     public void setMovieListener(@Nullable MovieListener movieListener) {
    261         mMovieListener = movieListener;
    262     }
    263 
    264     /**
    265      * The raw resource id of the video to play.
    266      *
    267      * @return ID of the video resource.
    268      */
    269     public int getVideoResourceId() {
    270         return mVideoResourceId;
    271     }
    272 
    273     /**
    274      * Sets the raw resource ID of video to play.
    275      *
    276      * @param id The raw resource ID.
    277      */
    278     public void setVideoResourceId(@RawRes int id) {
    279         if (id == mVideoResourceId) {
    280             return;
    281         }
    282         mVideoResourceId = id;
    283         Surface surface = mSurfaceView.getHolder().getSurface();
    284         if (surface != null && surface.isValid()) {
    285             closeVideo();
    286             openVideo(surface);
    287         }
    288     }
    289 
    290     public void setAdjustViewBounds(boolean adjustViewBounds) {
    291         if (mAdjustViewBounds == adjustViewBounds) {
    292             return;
    293         }
    294         mAdjustViewBounds = adjustViewBounds;
    295         if (adjustViewBounds) {
    296             setBackground(null);
    297         } else {
    298             setBackgroundColor(Color.BLACK);
    299         }
    300         requestLayout();
    301     }
    302 
    303     /** Shows all the controls. */
    304     public void showControls() {
    305         TransitionManager.beginDelayedTransition(this);
    306         mShade.setVisibility(View.VISIBLE);
    307         mToggle.setVisibility(View.VISIBLE);
    308         mFastForward.setVisibility(View.VISIBLE);
    309         mFastRewind.setVisibility(View.VISIBLE);
    310         mMinimize.setVisibility(View.VISIBLE);
    311     }
    312 
    313     /** Hides all the controls. */
    314     public void hideControls() {
    315         TransitionManager.beginDelayedTransition(this);
    316         mShade.setVisibility(View.INVISIBLE);
    317         mToggle.setVisibility(View.INVISIBLE);
    318         mFastForward.setVisibility(View.INVISIBLE);
    319         mFastRewind.setVisibility(View.INVISIBLE);
    320         mMinimize.setVisibility(View.INVISIBLE);
    321     }
    322 
    323     /** Fast-forward the video. */
    324     public void fastForward() {
    325         if (mMediaPlayer == null) {
    326             return;
    327         }
    328         mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() + FAST_FORWARD_REWIND_INTERVAL);
    329     }
    330 
    331     /** Fast-rewind the video. */
    332     public void fastRewind() {
    333         if (mMediaPlayer == null) {
    334             return;
    335         }
    336         mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() - FAST_FORWARD_REWIND_INTERVAL);
    337     }
    338 
    339     /**
    340      * Returns the current position of the video. If the the player has not been created, then
    341      * assumes the beginning of the video.
    342      *
    343      * @return The current position of the video.
    344      */
    345     public int getCurrentPosition() {
    346         if (mMediaPlayer == null) {
    347             return 0;
    348         }
    349         return mMediaPlayer.getCurrentPosition();
    350     }
    351 
    352     public boolean isPlaying() {
    353         return mMediaPlayer != null && mMediaPlayer.isPlaying();
    354     }
    355 
    356     public void play() {
    357         if (mMediaPlayer == null) {
    358             return;
    359         }
    360         mMediaPlayer.start();
    361         adjustToggleState();
    362         setKeepScreenOn(true);
    363         if (mMovieListener != null) {
    364             mMovieListener.onMovieStarted();
    365         }
    366     }
    367 
    368     public void pause() {
    369         if (mMediaPlayer == null) {
    370             adjustToggleState();
    371             return;
    372         }
    373         mMediaPlayer.pause();
    374         adjustToggleState();
    375         setKeepScreenOn(false);
    376         if (mMovieListener != null) {
    377             mMovieListener.onMovieStopped();
    378         }
    379     }
    380 
    381     void openVideo(Surface surface) {
    382         if (mVideoResourceId == 0) {
    383             return;
    384         }
    385         mMediaPlayer = new MediaPlayer();
    386         mMediaPlayer.setSurface(surface);
    387         startVideo();
    388     }
    389 
    390     /** Restarts playback of the video. */
    391     public void startVideo() {
    392         mMediaPlayer.reset();
    393         try (AssetFileDescriptor fd = getResources().openRawResourceFd(mVideoResourceId)) {
    394             mMediaPlayer.setDataSource(fd);
    395             mMediaPlayer.setOnPreparedListener(
    396                     new MediaPlayer.OnPreparedListener() {
    397                         @Override
    398                         public void onPrepared(MediaPlayer mediaPlayer) {
    399                             // Adjust the aspect ratio of this view
    400                             requestLayout();
    401                             if (mSavedCurrentPosition > 0) {
    402                                 mediaPlayer.seekTo(mSavedCurrentPosition);
    403                                 mSavedCurrentPosition = 0;
    404                             } else {
    405                                 // Start automatically
    406                                 play();
    407                             }
    408                         }
    409                     });
    410             mMediaPlayer.setOnCompletionListener(
    411                     new MediaPlayer.OnCompletionListener() {
    412                         @Override
    413                         public void onCompletion(MediaPlayer mediaPlayer) {
    414                             adjustToggleState();
    415                             setKeepScreenOn(false);
    416                             if (mMovieListener != null) {
    417                                 mMovieListener.onMovieStopped();
    418                             }
    419                         }
    420                     });
    421             mMediaPlayer.prepare();
    422         } catch (IOException e) {
    423             Log.e(TAG, "Failed to open video", e);
    424         }
    425     }
    426 
    427     void closeVideo() {
    428         if (mMediaPlayer != null) {
    429             mMediaPlayer.release();
    430             mMediaPlayer = null;
    431         }
    432     }
    433 
    434     void toggle() {
    435         if (mMediaPlayer == null) {
    436             return;
    437         }
    438         if (mMediaPlayer.isPlaying()) {
    439             pause();
    440         } else {
    441             play();
    442         }
    443     }
    444 
    445     void toggleControls() {
    446         if (mShade.getVisibility() == View.VISIBLE) {
    447             hideControls();
    448         } else {
    449             showControls();
    450         }
    451     }
    452 
    453     void adjustToggleState() {
    454         if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
    455             mToggle.setContentDescription(getResources().getString(R.string.pause));
    456             mToggle.setImageResource(R.drawable.ic_pause_64dp);
    457         } else {
    458             mToggle.setContentDescription(getResources().getString(R.string.play));
    459             mToggle.setImageResource(R.drawable.ic_play_arrow_64dp);
    460         }
    461     }
    462 
    463     private static class TimeoutHandler extends Handler {
    464 
    465         static final int MESSAGE_HIDE_CONTROLS = 1;
    466 
    467         private final WeakReference<MovieView> mMovieViewRef;
    468 
    469         TimeoutHandler(MovieView view) {
    470             mMovieViewRef = new WeakReference<>(view);
    471         }
    472 
    473         @Override
    474         public void handleMessage(Message msg) {
    475             switch (msg.what) {
    476                 case MESSAGE_HIDE_CONTROLS:
    477                     MovieView movieView = mMovieViewRef.get();
    478                     if (movieView != null) {
    479                         movieView.hideControls();
    480                     }
    481                     break;
    482                 default:
    483                     super.handleMessage(msg);
    484             }
    485         }
    486     }
    487 }
    488