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