blob: 3de534b97efd2d55c4ed724f42364886138d27f7 [file] [log] [blame]
Owen Lina2fba682011-08-17 22:07:43 +08001/*
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
Owen Lina2fba682011-08-17 22:07:43 +080019import 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;
Owen Lin0fdbf232011-09-14 19:49:03 +080031import android.os.Bundle;
Owen Lina2fba682011-08-17 22:07:43 +080032import android.os.Handler;
Chih-Chung Changb5947c52011-11-29 14:21:11 +080033import android.view.KeyEvent;
Chih-Chung Chang6645e872011-10-14 16:09:09 +080034import android.view.MotionEvent;
Owen Lina2fba682011-08-17 22:07:43 +080035import android.view.View;
Chih-Chung Chang6645e872011-10-14 16:09:09 +080036import android.view.ViewGroup;
Owen Lina2fba682011-08-17 22:07:43 +080037import android.widget.VideoView;
38
Owen Lin50dab502011-11-11 12:01:09 +080039import com.android.gallery3d.R;
40import com.android.gallery3d.common.BlobCache;
41import com.android.gallery3d.util.CacheManager;
42import com.android.gallery3d.util.GalleryUtils;
43
Owen Lina2fba682011-08-17 22:07:43 +080044import java.io.ByteArrayInputStream;
45import java.io.ByteArrayOutputStream;
46import java.io.DataInputStream;
47import java.io.DataOutputStream;
48
49public class MoviePlayer implements
Chih-Chung Chang6645e872011-10-14 16:09:09 +080050 MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
51 ControllerOverlay.Listener {
Owen Lina2fba682011-08-17 22:07:43 +080052 @SuppressWarnings("unused")
53 private static final String TAG = "MoviePlayer";
54
Owen Lin0fdbf232011-09-14 19:49:03 +080055 private static final String KEY_VIDEO_POSITION = "video-position";
56 private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
57
Owen Lina2fba682011-08-17 22:07:43 +080058 // 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
Owen Lin0fdbf232011-09-14 19:49:03 +080063 // 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
Owen Lina2fba682011-08-17 22:07:43 +080067 private Context mContext;
68 private final VideoView mVideoView;
Owen Lina2fba682011-08-17 22:07:43 +080069 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;
Chih-Chung Chang6645e872011-10-14 16:09:09 +080074 private final ControllerOverlay mController;
Owen Lina2fba682011-08-17 22:07:43 +080075
Owen Lin0fdbf232011-09-14 19:49:03 +080076 private long mResumeableTime = Long.MAX_VALUE;
77 private int mVideoPosition = 0;
78 private boolean mHasPaused = false;
Owen Lina2fba682011-08-17 22:07:43 +080079
Chih-Chung Chang6645e872011-10-14 16:09:09 +080080 // If the time bar is being dragged.
81 private boolean mDragging;
82
83 // If the time bar is visible.
84 private boolean mShowing;
85
Owen Lina2fba682011-08-17 22:07:43 +080086 private final Runnable mPlayingChecker = new Runnable() {
Owen Lin0fdbf232011-09-14 19:49:03 +080087 @Override
Owen Lina2fba682011-08-17 22:07:43 +080088 public void run() {
89 if (mVideoView.isPlaying()) {
Chih-Chung Chang6645e872011-10-14 16:09:09 +080090 mController.showPlaying();
Owen Lina2fba682011-08-17 22:07:43 +080091 } else {
92 mHandler.postDelayed(mPlayingChecker, 250);
93 }
94 }
95 };
96
Chih-Chung Chang6645e872011-10-14 16:09:09 +080097 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
Owen Lin0fdbf232011-09-14 19:49:03 +0800105 public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800106 Bundle savedInstance, boolean canReplay) {
Owen Lina2fba682011-08-17 22:07:43 +0800107 mContext = movieActivity.getApplicationContext();
108 mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
Owen Lina2fba682011-08-17 22:07:43 +0800109 mBookmarker = new Bookmarker(movieActivity);
110 mActionBar = movieActivity.getActionBar();
111 mUri = videoUri;
112
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800113 mController = new MovieControllerOverlay(mContext);
114 ((ViewGroup)rootView).addView(mController.getView());
115 mController.setListener(this);
116 mController.setCanReplay(canReplay);
Owen Lina2fba682011-08-17 22:07:43 +0800117
118 mVideoView.setOnErrorListener(this);
119 mVideoView.setOnCompletionListener(this);
120 mVideoView.setVideoURI(mUri);
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800121 mVideoView.setOnTouchListener(new View.OnTouchListener() {
122 public boolean onTouch(View v, MotionEvent event) {
123 mController.show();
124 return true;
Owen Lina2fba682011-08-17 22:07:43 +0800125 }
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800126 });
Chih-Chung Chang399bb7e2011-09-27 16:50:16 +0800127
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) {
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800135 mController.show();
Chih-Chung Chang399bb7e2011-09-27 16:50:16 +0800136 }
137 }
138 });
139
Owen Lina2fba682011-08-17 22:07:43 +0800140 mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
141 mAudioBecomingNoisyReceiver.register();
142
Owen Lina2fba682011-08-17 22:07:43 +0800143 Intent i = new Intent(SERVICECMD);
144 i.putExtra(CMDNAME, CMDPAUSE);
145 movieActivity.sendBroadcast(i);
146
Owen Lin0fdbf232011-09-14 19:49:03 +0800147 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);
Owen Lina2fba682011-08-17 22:07:43 +0800150 mVideoView.start();
Owen Lin0fdbf232011-09-14 19:49:03 +0800151 mVideoView.suspend();
152 mHasPaused = true;
153 } else {
154 final Integer bookmark = mBookmarker.getBookmark(mUri);
155 if (bookmark != null) {
156 showResumeDialog(movieActivity, bookmark);
157 } else {
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800158 startVideo();
Owen Lin0fdbf232011-09-14 19:49:03 +0800159 }
Owen Lina2fba682011-08-17 22:07:43 +0800160 }
161 }
162
Chih-Chung Chang399bb7e2011-09-27 16:50:16 +0800163 private void showSystemUi(boolean visible) {
Chih-Chung Chang9d9226a2011-11-09 20:57:53 +0800164 int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
165 View.SYSTEM_UI_FLAG_LOW_PROFILE;
Chih-Chung Chang399bb7e2011-09-27 16:50:16 +0800166 mVideoView.setSystemUiVisibility(flag);
Chih-Chung Chang399bb7e2011-09-27 16:50:16 +0800167 }
168
Owen Lin0fdbf232011-09-14 19:49:03 +0800169 public void onSaveInstanceState(Bundle outState) {
170 outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
171 outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
172 }
173
Owen Lina2fba682011-08-17 22:07:43 +0800174 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() {
Owen Lin0fdbf232011-09-14 19:49:03 +0800181 @Override
Owen Lina2fba682011-08-17 22:07:43 +0800182 public void onCancel(DialogInterface dialog) {
183 onCompletion();
184 }
185 });
186 builder.setPositiveButton(
187 R.string.resume_playing_resume, new OnClickListener() {
Owen Lin0fdbf232011-09-14 19:49:03 +0800188 @Override
Owen Lina2fba682011-08-17 22:07:43 +0800189 public void onClick(DialogInterface dialog, int which) {
190 mVideoView.seekTo(bookmark);
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800191 startVideo();
Owen Lina2fba682011-08-17 22:07:43 +0800192 }
193 });
194 builder.setNegativeButton(
195 R.string.resume_playing_restart, new OnClickListener() {
Owen Lin0fdbf232011-09-14 19:49:03 +0800196 @Override
Owen Lina2fba682011-08-17 22:07:43 +0800197 public void onClick(DialogInterface dialog, int which) {
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800198 startVideo();
Owen Lina2fba682011-08-17 22:07:43 +0800199 }
200 });
201 builder.show();
202 }
203
204 public void onPause() {
Owen Lina2fba682011-08-17 22:07:43 +0800205 mHasPaused = true;
Owen Lin0fdbf232011-09-14 19:49:03 +0800206 mHandler.removeCallbacksAndMessages(null);
207 mVideoPosition = mVideoView.getCurrentPosition();
208 mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
209 mVideoView.suspend();
210 mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
Owen Lina2fba682011-08-17 22:07:43 +0800211 }
212
213 public void onResume() {
214 if (mHasPaused) {
Owen Lin0fdbf232011-09-14 19:49:03 +0800215 mVideoView.seekTo(mVideoPosition);
216 mVideoView.resume();
217
218 // If we have slept for too long, pause the play
219 if (System.currentTimeMillis() > mResumeableTime) {
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800220 pauseVideo();
Owen Lina2fba682011-08-17 22:07:43 +0800221 }
222 }
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800223 mHandler.post(mProgressChecker);
Owen Lina2fba682011-08-17 22:07:43 +0800224 }
225
226 public void onDestroy() {
227 mVideoView.stopPlayback();
228 mAudioBecomingNoisyReceiver.unregister();
229 }
230
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800231 // 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
Owen Lina2fba682011-08-17 22:07:43 +0800273 public boolean onError(MediaPlayer player, int arg1, int arg2) {
274 mHandler.removeCallbacksAndMessages(null);
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800275 // VideoView will show an error dialog if we return false, so no need
276 // to show more message.
277 mController.showErrorMessage("");
Owen Lina2fba682011-08-17 22:07:43 +0800278 return false;
279 }
280
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800281 @Override
Owen Lina2fba682011-08-17 22:07:43 +0800282 public void onCompletion(MediaPlayer mp) {
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800283 mController.showEnded();
Owen Lina2fba682011-08-17 22:07:43 +0800284 onCompletion();
285 }
286
287 public void onCompletion() {
288 }
289
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800290 // 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
Chih-Chung Changb5947c52011-11-29 14:21:11 +0800337 // 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
Chih-Chung Chang6645e872011-10-14 16:09:09 +0800386 // We want to pause when the headset is unplugged.
Owen Lina2fba682011-08-17 22:07:43 +0800387 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) {
Owen Lin50dab502011-11-11 12:01:09 +0800400 if (mVideoView.isPlaying()) pauseVideo();
Owen Lina2fba682011-08-17 22:07:43 +0800401 }
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}