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