Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2009 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.gallery3d.app;
     18 
     19 import android.app.AlertDialog;
     20 import android.content.BroadcastReceiver;
     21 import android.content.Context;
     22 import android.content.DialogInterface;
     23 import android.content.DialogInterface.OnCancelListener;
     24 import android.content.DialogInterface.OnClickListener;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.graphics.Color;
     28 import android.media.AudioManager;
     29 import android.media.MediaPlayer;
     30 import android.net.Uri;
     31 import android.os.Bundle;
     32 import android.os.Handler;
     33 import android.view.KeyEvent;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.VideoView;
     38 
     39 import com.android.gallery3d.R;
     40 import com.android.gallery3d.common.BlobCache;
     41 import com.android.gallery3d.util.CacheManager;
     42 import com.android.gallery3d.util.GalleryUtils;
     43 
     44 import java.io.ByteArrayInputStream;
     45 import java.io.ByteArrayOutputStream;
     46 import java.io.DataInputStream;
     47 import java.io.DataOutputStream;
     48 
     49 public class MoviePlayer implements
     50         MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
     51         ControllerOverlay.Listener {
     52     @SuppressWarnings("unused")
     53     private static final String TAG = "MoviePlayer";
     54 
     55     private static final String KEY_VIDEO_POSITION = "video-position";
     56     private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
     57 
     58     // Copied from MediaPlaybackService in the Music Player app.
     59     private static final String SERVICECMD = "com.android.music.musicservicecommand";
     60     private static final String CMDNAME = "command";
     61     private static final String CMDPAUSE = "pause";
     62 
     63     private static final long BLACK_TIMEOUT = 500;
     64 
     65     // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
     66     // Otherwise, we pause the player.
     67     private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
     68 
     69     private Context mContext;
     70     private final VideoView mVideoView;
     71     private final View mRootView;
     72     private final Bookmarker mBookmarker;
     73     private final Uri mUri;
     74     private final Handler mHandler = new Handler();
     75     private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
     76     private final MovieControllerOverlay mController;
     77 
     78     private long mResumeableTime = Long.MAX_VALUE;
     79     private int mVideoPosition = 0;
     80     private boolean mHasPaused = false;
     81     private int mLastSystemUiVis = 0;
     82 
     83     // If the time bar is being dragged.
     84     private boolean mDragging;
     85 
     86     // If the time bar is visible.
     87     private boolean mShowing;
     88 
     89     private final Runnable mPlayingChecker = new Runnable() {
     90         @Override
     91         public void run() {
     92             if (mVideoView.isPlaying()) {
     93                 mController.showPlaying();
     94             } else {
     95                 mHandler.postDelayed(mPlayingChecker, 250);
     96             }
     97         }
     98     };
     99 
    100     private final Runnable mRemoveBackground = new Runnable() {
    101         @Override
    102         public void run() {
    103             mRootView.setBackground(null);
    104         }
    105     };
    106 
    107     private final Runnable mProgressChecker = new Runnable() {
    108         @Override
    109         public void run() {
    110             int pos = setProgress();
    111             mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
    112         }
    113     };
    114 
    115     public MoviePlayer(View rootView, final MovieActivity movieActivity,
    116             Uri videoUri, Bundle savedInstance, boolean canReplay) {
    117         mContext = movieActivity.getApplicationContext();
    118         mRootView = rootView;
    119         mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
    120         mBookmarker = new Bookmarker(movieActivity);
    121         mUri = videoUri;
    122 
    123         mController = new MovieControllerOverlay(mContext);
    124         ((ViewGroup)rootView).addView(mController.getView());
    125         mController.setListener(this);
    126         mController.setCanReplay(canReplay);
    127 
    128         mVideoView.setOnErrorListener(this);
    129         mVideoView.setOnCompletionListener(this);
    130         mVideoView.setVideoURI(mUri);
    131         mVideoView.setOnTouchListener(new View.OnTouchListener() {
    132             @Override
    133             public boolean onTouch(View v, MotionEvent event) {
    134                 mController.show();
    135                 return true;
    136             }
    137         });
    138 
    139         // The SurfaceView is transparent before drawing the first frame.
    140         // This makes the UI flashing when open a video. (black -> old screen
    141         // -> video) However, we have no way to know the timing of the first
    142         // frame. So, we hide the VideoView for a while to make sure the
    143         // video has been drawn on it.
    144         mVideoView.postDelayed(new Runnable() {
    145             @Override
    146             public void run() {
    147                 mVideoView.setVisibility(View.VISIBLE);
    148             }
    149         }, BLACK_TIMEOUT);
    150 
    151         // When the user touches the screen or uses some hard key, the framework
    152         // will change system ui visibility from invisible to visible. We show
    153         // the media control and enable system UI (e.g. ActionBar) to be visible at this point
    154         mVideoView.setOnSystemUiVisibilityChangeListener(
    155                 new View.OnSystemUiVisibilityChangeListener() {
    156             @Override
    157             public void onSystemUiVisibilityChange(int visibility) {
    158                 int diff = mLastSystemUiVis ^ visibility;
    159                 mLastSystemUiVis = visibility;
    160                 if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
    161                         && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
    162                     mController.show();
    163 
    164                     // We need to set the background to clear ghosting images
    165                     // when ActionBar slides in. However, if we keep the background,
    166                     // there will be one additional layer in HW composer, which is bad
    167                     // to battery. As a solution, we remove the background when we
    168                     // hide the action bar
    169                     mHandler.removeCallbacks(mRemoveBackground);
    170                     mRootView.setBackgroundColor(Color.BLACK);
    171                 } else {
    172                     mHandler.removeCallbacks(mRemoveBackground);
    173 
    174                     // Wait for the slide out animation, one second should be enough
    175                     mHandler.postDelayed(mRemoveBackground, 1000);
    176                 }
    177             }
    178         });
    179 
    180         // Hide system UI by default
    181         showSystemUi(false);
    182 
    183         mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
    184         mAudioBecomingNoisyReceiver.register();
    185 
    186         Intent i = new Intent(SERVICECMD);
    187         i.putExtra(CMDNAME, CMDPAUSE);
    188         movieActivity.sendBroadcast(i);
    189 
    190         if (savedInstance != null) { // this is a resumed activity
    191             mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
    192             mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
    193             mVideoView.start();
    194             mVideoView.suspend();
    195             mHasPaused = true;
    196         } else {
    197             final Integer bookmark = mBookmarker.getBookmark(mUri);
    198             if (bookmark != null) {
    199                 showResumeDialog(movieActivity, bookmark);
    200             } else {
    201                 startVideo();
    202             }
    203         }
    204     }
    205 
    206     private void showSystemUi(boolean visible) {
    207         int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
    208                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    209                     | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
    210         if (!visible) {
    211             flag |= View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN
    212                     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
    213         }
    214         mVideoView.setSystemUiVisibility(flag);
    215     }
    216 
    217     public void onSaveInstanceState(Bundle outState) {
    218         outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
    219         outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
    220     }
    221 
    222     private void showResumeDialog(Context context, final int bookmark) {
    223         AlertDialog.Builder builder = new AlertDialog.Builder(context);
    224         builder.setTitle(R.string.resume_playing_title);
    225         builder.setMessage(String.format(
    226                 context.getString(R.string.resume_playing_message),
    227                 GalleryUtils.formatDuration(context, bookmark / 1000)));
    228         builder.setOnCancelListener(new OnCancelListener() {
    229             @Override
    230             public void onCancel(DialogInterface dialog) {
    231                 onCompletion();
    232             }
    233         });
    234         builder.setPositiveButton(
    235                 R.string.resume_playing_resume, new OnClickListener() {
    236             @Override
    237             public void onClick(DialogInterface dialog, int which) {
    238                 mVideoView.seekTo(bookmark);
    239                 startVideo();
    240             }
    241         });
    242         builder.setNegativeButton(
    243                 R.string.resume_playing_restart, new OnClickListener() {
    244             @Override
    245             public void onClick(DialogInterface dialog, int which) {
    246                 startVideo();
    247             }
    248         });
    249         builder.show();
    250     }
    251 
    252     public void onPause() {
    253         mHasPaused = true;
    254         mHandler.removeCallbacksAndMessages(null);
    255         mVideoPosition = mVideoView.getCurrentPosition();
    256         mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
    257         mVideoView.suspend();
    258         mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
    259     }
    260 
    261     public void onResume() {
    262         if (mHasPaused) {
    263             mVideoView.seekTo(mVideoPosition);
    264             mVideoView.resume();
    265 
    266             // If we have slept for too long, pause the play
    267             if (System.currentTimeMillis() > mResumeableTime) {
    268                 pauseVideo();
    269             }
    270         }
    271         mHandler.post(mProgressChecker);
    272     }
    273 
    274     public void onDestroy() {
    275         mVideoView.stopPlayback();
    276         mAudioBecomingNoisyReceiver.unregister();
    277     }
    278 
    279     // This updates the time bar display (if necessary). It is called every
    280     // second by mProgressChecker and also from places where the time bar needs
    281     // to be updated immediately.
    282     private int setProgress() {
    283         if (mDragging || !mShowing) {
    284             return 0;
    285         }
    286         int position = mVideoView.getCurrentPosition();
    287         int duration = mVideoView.getDuration();
    288         mController.setTimes(position, duration);
    289         return position;
    290     }
    291 
    292     private void startVideo() {
    293         // For streams that we expect to be slow to start up, show a
    294         // progress spinner until playback starts.
    295         String scheme = mUri.getScheme();
    296         if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
    297             mController.showLoading();
    298             mHandler.removeCallbacks(mPlayingChecker);
    299             mHandler.postDelayed(mPlayingChecker, 250);
    300         } else {
    301             mController.showPlaying();
    302             mController.hide();
    303         }
    304 
    305         mVideoView.start();
    306         setProgress();
    307     }
    308 
    309     private void playVideo() {
    310         mVideoView.start();
    311         mController.showPlaying();
    312         setProgress();
    313     }
    314 
    315     private void pauseVideo() {
    316         mVideoView.pause();
    317         mController.showPaused();
    318     }
    319 
    320     // Below are notifications from VideoView
    321     @Override
    322     public boolean onError(MediaPlayer player, int arg1, int arg2) {
    323         mHandler.removeCallbacksAndMessages(null);
    324         // VideoView will show an error dialog if we return false, so no need
    325         // to show more message.
    326         mController.showErrorMessage("");
    327         return false;
    328     }
    329 
    330     @Override
    331     public void onCompletion(MediaPlayer mp) {
    332         mController.showEnded();
    333         onCompletion();
    334     }
    335 
    336     public void onCompletion() {
    337     }
    338 
    339     // Below are notifications from ControllerOverlay
    340     @Override
    341     public void onPlayPause() {
    342         if (mVideoView.isPlaying()) {
    343             pauseVideo();
    344         } else {
    345             playVideo();
    346         }
    347     }
    348 
    349     @Override
    350     public void onSeekStart() {
    351         mDragging = true;
    352     }
    353 
    354     @Override
    355     public void onSeekMove(int time) {
    356         mVideoView.seekTo(time);
    357     }
    358 
    359     @Override
    360     public void onSeekEnd(int time) {
    361         mDragging = false;
    362         mVideoView.seekTo(time);
    363         setProgress();
    364     }
    365 
    366     @Override
    367     public void onShown() {
    368         mShowing = true;
    369         setProgress();
    370         showSystemUi(true);
    371     }
    372 
    373     @Override
    374     public void onHidden() {
    375         mShowing = false;
    376         showSystemUi(false);
    377     }
    378 
    379     @Override
    380     public void onReplay() {
    381         startVideo();
    382     }
    383 
    384     // Below are key events passed from MovieActivity.
    385     public boolean onKeyDown(int keyCode, KeyEvent event) {
    386 
    387         // Some headsets will fire off 7-10 events on a single click
    388         if (event.getRepeatCount() > 0) {
    389             return isMediaKey(keyCode);
    390         }
    391 
    392         switch (keyCode) {
    393             case KeyEvent.KEYCODE_HEADSETHOOK:
    394             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
    395                 if (mVideoView.isPlaying()) {
    396                     pauseVideo();
    397                 } else {
    398                     playVideo();
    399                 }
    400                 return true;
    401             case KeyEvent.KEYCODE_MEDIA_PAUSE:
    402                 if (mVideoView.isPlaying()) {
    403                     pauseVideo();
    404                 }
    405                 return true;
    406             case KeyEvent.KEYCODE_MEDIA_PLAY:
    407                 if (!mVideoView.isPlaying()) {
    408                     playVideo();
    409                 }
    410                 return true;
    411             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
    412             case KeyEvent.KEYCODE_MEDIA_NEXT:
    413                 // TODO: Handle next / previous accordingly, for now we're
    414                 // just consuming the events.
    415                 return true;
    416         }
    417         return false;
    418     }
    419 
    420     public boolean onKeyUp(int keyCode, KeyEvent event) {
    421         return isMediaKey(keyCode);
    422     }
    423 
    424     private static boolean isMediaKey(int keyCode) {
    425         return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
    426                 || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
    427                 || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
    428                 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
    429                 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
    430                 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
    431     }
    432 
    433     // We want to pause when the headset is unplugged.
    434     private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
    435 
    436         public void register() {
    437             mContext.registerReceiver(this,
    438                     new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
    439         }
    440 
    441         public void unregister() {
    442             mContext.unregisterReceiver(this);
    443         }
    444 
    445         @Override
    446         public void onReceive(Context context, Intent intent) {
    447             if (mVideoView.isPlaying()) pauseVideo();
    448         }
    449     }
    450 }
    451 
    452 class Bookmarker {
    453     private static final String TAG = "Bookmarker";
    454 
    455     private static final String BOOKMARK_CACHE_FILE = "bookmark";
    456     private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
    457     private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
    458     private static final int BOOKMARK_CACHE_VERSION = 1;
    459 
    460     private static final int HALF_MINUTE = 30 * 1000;
    461     private static final int TWO_MINUTES = 4 * HALF_MINUTE;
    462 
    463     private final Context mContext;
    464 
    465     public Bookmarker(Context context) {
    466         mContext = context;
    467     }
    468 
    469     public void setBookmark(Uri uri, int bookmark, int duration) {
    470         try {
    471             BlobCache cache = CacheManager.getCache(mContext,
    472                     BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
    473                     BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
    474 
    475             ByteArrayOutputStream bos = new ByteArrayOutputStream();
    476             DataOutputStream dos = new DataOutputStream(bos);
    477             dos.writeUTF(uri.toString());
    478             dos.writeInt(bookmark);
    479             dos.writeInt(duration);
    480             dos.flush();
    481             cache.insert(uri.hashCode(), bos.toByteArray());
    482         } catch (Throwable t) {
    483             Log.w(TAG, "setBookmark failed", t);
    484         }
    485     }
    486 
    487     public Integer getBookmark(Uri uri) {
    488         try {
    489             BlobCache cache = CacheManager.getCache(mContext,
    490                     BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
    491                     BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
    492 
    493             byte[] data = cache.lookup(uri.hashCode());
    494             if (data == null) return null;
    495 
    496             DataInputStream dis = new DataInputStream(
    497                     new ByteArrayInputStream(data));
    498 
    499             String uriString = dis.readUTF(dis);
    500             int bookmark = dis.readInt();
    501             int duration = dis.readInt();
    502 
    503             if (!uriString.equals(uri.toString())) {
    504                 return null;
    505             }
    506 
    507             if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
    508                     || (bookmark > (duration - HALF_MINUTE))) {
    509                 return null;
    510             }
    511             return Integer.valueOf(bookmark);
    512         } catch (Throwable t) {
    513             Log.w(TAG, "getBookmark failed", t);
    514         }
    515         return null;
    516     }
    517 }
    518