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