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