MoviePlayer.java revision 540e4698824654232f300992ed8c03b20c9d5946
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 com.android.gallery3d.R;
20import com.android.gallery3d.common.BlobCache;
21import com.android.gallery3d.util.CacheManager;
22import com.android.gallery3d.util.GalleryUtils;
23
24import android.app.ActionBar;
25import android.app.AlertDialog;
26import android.content.BroadcastReceiver;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.DialogInterface.OnCancelListener;
30import android.content.DialogInterface.OnClickListener;
31import android.content.Intent;
32import android.content.IntentFilter;
33import android.media.AudioManager;
34import android.media.MediaPlayer;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.view.KeyEvent;
39import android.view.View;
40import android.widget.MediaController;
41import android.widget.VideoView;
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    @SuppressWarnings("unused")
51    private static final String TAG = "MoviePlayer";
52
53    private static final String KEY_VIDEO_POSITION = "video-position";
54    private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
55
56    // Copied from MediaPlaybackService in the Music Player app.
57    private static final String SERVICECMD = "com.android.music.musicservicecommand";
58    private static final String CMDNAME = "command";
59    private static final String CMDPAUSE = "pause";
60
61    // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
62    // Otherwise, we pause the player.
63    private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
64
65    private Context mContext;
66    private final VideoView mVideoView;
67    private final View mProgressView;
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 MediaController mMediaController;
74
75    private long mResumeableTime = Long.MAX_VALUE;
76    private int mVideoPosition = 0;
77    private boolean mHasPaused = false;
78
79    private final Runnable mPlayingChecker = new Runnable() {
80        @Override
81        public void run() {
82            if (mVideoView.isPlaying()) {
83                mProgressView.setVisibility(View.GONE);
84            } else {
85                mHandler.postDelayed(mPlayingChecker, 250);
86            }
87        }
88    };
89
90    public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
91            Bundle savedInstance) {
92        mContext = movieActivity.getApplicationContext();
93        mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
94        mProgressView = rootView.findViewById(R.id.progress_indicator);
95        mBookmarker = new Bookmarker(movieActivity);
96        mActionBar = movieActivity.getActionBar();
97        mUri = videoUri;
98
99        // For streams that we expect to be slow to start up, show a
100        // progress spinner until playback starts.
101        String scheme = mUri.getScheme();
102        if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
103            mHandler.postDelayed(mPlayingChecker, 250);
104        } else {
105            mProgressView.setVisibility(View.GONE);
106        }
107
108        mVideoView.setOnErrorListener(this);
109        mVideoView.setOnCompletionListener(this);
110        mVideoView.setVideoURI(mUri);
111
112        mMediaController = new MediaController(movieActivity) {
113            @Override
114            public void show() {
115                super.show();
116                mActionBar.show();
117            }
118
119            @Override
120            public void hide() {
121                super.hide();
122                mActionBar.hide();
123            }
124        };
125        mMediaController.setOnKeyListener(new View.OnKeyListener() {
126            @Override
127            public boolean onKey(View v, int keyCode, KeyEvent event) {
128                if (keyCode == KeyEvent.KEYCODE_BACK) {
129                    if (event.getAction() == KeyEvent.ACTION_UP) {
130                        movieActivity.onBackPressed();
131                    }
132                    return true;
133                }
134                return false;
135            }
136        });
137        mVideoView.setMediaController(mMediaController);
138
139        mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
140        mAudioBecomingNoisyReceiver.register();
141
142        // make the video view handle keys for seeking and pausing
143        mVideoView.requestFocus();
144
145        Intent i = new Intent(SERVICECMD);
146        i.putExtra(CMDNAME, CMDPAUSE);
147        movieActivity.sendBroadcast(i);
148
149        if (savedInstance != null) { // this is a resumed activity
150            mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
151            mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
152            mVideoView.start();
153            mVideoView.suspend();
154            mHasPaused = true;
155        } else {
156            final Integer bookmark = mBookmarker.getBookmark(mUri);
157            if (bookmark != null) {
158                showResumeDialog(movieActivity, bookmark);
159            } else {
160                mVideoView.start();
161            }
162        }
163    }
164
165    public void onSaveInstanceState(Bundle outState) {
166        outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
167        outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
168    }
169
170    private void showResumeDialog(Context context, final int bookmark) {
171        AlertDialog.Builder builder = new AlertDialog.Builder(context);
172        builder.setTitle(R.string.resume_playing_title);
173        builder.setMessage(String.format(
174                context.getString(R.string.resume_playing_message),
175                GalleryUtils.formatDuration(context, bookmark / 1000)));
176        builder.setOnCancelListener(new OnCancelListener() {
177            @Override
178            public void onCancel(DialogInterface dialog) {
179                onCompletion();
180            }
181        });
182        builder.setPositiveButton(
183                R.string.resume_playing_resume, new OnClickListener() {
184            @Override
185            public void onClick(DialogInterface dialog, int which) {
186                mVideoView.seekTo(bookmark);
187                mVideoView.start();
188            }
189        });
190        builder.setNegativeButton(
191                R.string.resume_playing_restart, new OnClickListener() {
192            @Override
193            public void onClick(DialogInterface dialog, int which) {
194                mVideoView.start();
195            }
196        });
197        builder.show();
198    }
199
200    public void onPause() {
201        mHasPaused = true;
202        mHandler.removeCallbacksAndMessages(null);
203        mVideoPosition = mVideoView.getCurrentPosition();
204        mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
205        mVideoView.suspend();
206        mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
207    }
208
209    public void onResume() {
210        if (mHasPaused) {
211            mVideoView.seekTo(mVideoPosition);
212            mVideoView.resume();
213
214            // If we have slept for too long, pause the play
215            if (System.currentTimeMillis() > mResumeableTime) {
216                mMediaController.show();
217                mVideoView.pause();
218            }
219        }
220    }
221
222    public void onDestroy() {
223        mVideoView.stopPlayback();
224        mAudioBecomingNoisyReceiver.unregister();
225    }
226
227    public boolean onError(MediaPlayer player, int arg1, int arg2) {
228        mHandler.removeCallbacksAndMessages(null);
229        mProgressView.setVisibility(View.GONE);
230        return false;
231    }
232
233    public void onCompletion(MediaPlayer mp) {
234        onCompletion();
235    }
236
237    public void onCompletion() {
238    }
239
240    private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
241
242        public void register() {
243            mContext.registerReceiver(this,
244                    new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
245        }
246
247        public void unregister() {
248            mContext.unregisterReceiver(this);
249        }
250
251        @Override
252        public void onReceive(Context context, Intent intent) {
253            if (mVideoView.isPlaying()) {
254                mVideoView.pause();
255          }
256        }
257    }
258}
259
260class Bookmarker {
261    private static final String TAG = "Bookmarker";
262
263    private static final String BOOKMARK_CACHE_FILE = "bookmark";
264    private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
265    private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
266    private static final int BOOKMARK_CACHE_VERSION = 1;
267
268    private static final int HALF_MINUTE = 30 * 1000;
269    private static final int TWO_MINUTES = 4 * HALF_MINUTE;
270
271    private final Context mContext;
272
273    public Bookmarker(Context context) {
274        mContext = context;
275    }
276
277    public void setBookmark(Uri uri, int bookmark, int duration) {
278        try {
279            BlobCache cache = CacheManager.getCache(mContext,
280                    BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
281                    BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
282
283            ByteArrayOutputStream bos = new ByteArrayOutputStream();
284            DataOutputStream dos = new DataOutputStream(bos);
285            dos.writeUTF(uri.toString());
286            dos.writeInt(bookmark);
287            dos.writeInt(duration);
288            dos.flush();
289            cache.insert(uri.hashCode(), bos.toByteArray());
290        } catch (Throwable t) {
291            Log.w(TAG, "setBookmark failed", t);
292        }
293    }
294
295    public Integer getBookmark(Uri uri) {
296        try {
297            BlobCache cache = CacheManager.getCache(mContext,
298                    BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
299                    BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
300
301            byte[] data = cache.lookup(uri.hashCode());
302            if (data == null) return null;
303
304            DataInputStream dis = new DataInputStream(
305                    new ByteArrayInputStream(data));
306
307            String uriString = dis.readUTF(dis);
308            int bookmark = dis.readInt();
309            int duration = dis.readInt();
310
311            if (!uriString.equals(uri.toString())) {
312                return null;
313            }
314
315            if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
316                    || (bookmark > (duration - HALF_MINUTE))) {
317                return null;
318            }
319            return Integer.valueOf(bookmark);
320        } catch (Throwable t) {
321            Log.w(TAG, "getBookmark failed", t);
322        }
323        return null;
324    }
325}
326