blob: 2c8f9eb367ebc03a25758906b4ac4a7d253721f5 [file] [log] [blame]
Owen Linf9a0a432011-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 Linf9a0a432011-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 Lin540e4692011-09-14 19:49:03 +080031import android.os.Bundle;
Owen Linf9a0a432011-08-17 22:07:43 +080032import android.os.Handler;
Chih-Chung Changfc8c5032011-11-29 14:21:11 +080033import android.view.KeyEvent;
Chih-Chung Chang209a9162011-10-14 16:09:09 +080034import android.view.MotionEvent;
Owen Linf9a0a432011-08-17 22:07:43 +080035import android.view.View;
Chih-Chung Chang209a9162011-10-14 16:09:09 +080036import android.view.ViewGroup;
Owen Linf9a0a432011-08-17 22:07:43 +080037import android.widget.VideoView;
38
Owen Linaea077a2011-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 Linf9a0a432011-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 Chang209a9162011-10-14 16:09:09 +080050 MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
51 ControllerOverlay.Listener {
Owen Linf9a0a432011-08-17 22:07:43 +080052 @SuppressWarnings("unused")
53 private static final String TAG = "MoviePlayer";
54
Owen Lin540e4692011-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 Linf9a0a432011-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 Lin540e4692011-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 Linf9a0a432011-08-17 22:07:43 +080067 private Context mContext;
68 private final VideoView mVideoView;
Owen Linf9a0a432011-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 Chang209a9162011-10-14 16:09:09 +080074 private final ControllerOverlay mController;
Owen Linf9a0a432011-08-17 22:07:43 +080075
Owen Lin540e4692011-09-14 19:49:03 +080076 private long mResumeableTime = Long.MAX_VALUE;
77 private int mVideoPosition = 0;
78 private boolean mHasPaused = false;
Owen Linf9a0a432011-08-17 22:07:43 +080079
Chih-Chung Chang209a9162011-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
Ray Chen392a2922012-05-14 17:19:56 +080086 // Control when system UI can be shown
87 private boolean mAllowShowingSystemUI;
88
Owen Linf9a0a432011-08-17 22:07:43 +080089 private final Runnable mPlayingChecker = new Runnable() {
Owen Lin540e4692011-09-14 19:49:03 +080090 @Override
Owen Linf9a0a432011-08-17 22:07:43 +080091 public void run() {
92 if (mVideoView.isPlaying()) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +080093 mController.showPlaying();
Owen Linf9a0a432011-08-17 22:07:43 +080094 } else {
95 mHandler.postDelayed(mPlayingChecker, 250);
96 }
97 }
98 };
99
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800100 private final Runnable mProgressChecker = new Runnable() {
101 @Override
102 public void run() {
103 int pos = setProgress();
104 mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
105 }
106 };
107
Owen Lin540e4692011-09-14 19:49:03 +0800108 public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800109 Bundle savedInstance, boolean canReplay) {
Owen Linf9a0a432011-08-17 22:07:43 +0800110 mContext = movieActivity.getApplicationContext();
111 mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
Owen Linf9a0a432011-08-17 22:07:43 +0800112 mBookmarker = new Bookmarker(movieActivity);
113 mActionBar = movieActivity.getActionBar();
114 mUri = videoUri;
115
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800116 mController = new MovieControllerOverlay(mContext);
117 ((ViewGroup)rootView).addView(mController.getView());
118 mController.setListener(this);
119 mController.setCanReplay(canReplay);
Owen Linf9a0a432011-08-17 22:07:43 +0800120
121 mVideoView.setOnErrorListener(this);
122 mVideoView.setOnCompletionListener(this);
123 mVideoView.setVideoURI(mUri);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800124 mVideoView.setOnTouchListener(new View.OnTouchListener() {
125 public boolean onTouch(View v, MotionEvent event) {
126 mController.show();
127 return true;
Owen Linf9a0a432011-08-17 22:07:43 +0800128 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800129 });
Chih-Chung Chang81760122011-09-27 16:50:16 +0800130
131 // When the user touches the screen or uses some hard key, the framework
132 // will change system ui visibility from invisible to visible. We show
Ray Chen392a2922012-05-14 17:19:56 +0800133 // the media control and enable system UI (e.g. ActionBar) to be visible at this point
Chih-Chung Chang81760122011-09-27 16:50:16 +0800134 mVideoView.setOnSystemUiVisibilityChangeListener(
135 new View.OnSystemUiVisibilityChangeListener() {
136 public void onSystemUiVisibilityChange(int visibility) {
137 if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
Ray Chen392a2922012-05-14 17:19:56 +0800138 mAllowShowingSystemUI = true;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800139 mController.show();
Chih-Chung Chang81760122011-09-27 16:50:16 +0800140 }
141 }
142 });
143
Ray Chen392a2922012-05-14 17:19:56 +0800144 // Hide system UI by default
145 showSystemUi(false);
146
Owen Linf9a0a432011-08-17 22:07:43 +0800147 mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
148 mAudioBecomingNoisyReceiver.register();
149
Owen Linf9a0a432011-08-17 22:07:43 +0800150 Intent i = new Intent(SERVICECMD);
151 i.putExtra(CMDNAME, CMDPAUSE);
152 movieActivity.sendBroadcast(i);
153
Owen Lin540e4692011-09-14 19:49:03 +0800154 if (savedInstance != null) { // this is a resumed activity
155 mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
156 mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
Owen Linf9a0a432011-08-17 22:07:43 +0800157 mVideoView.start();
Owen Lin540e4692011-09-14 19:49:03 +0800158 mVideoView.suspend();
159 mHasPaused = true;
160 } else {
161 final Integer bookmark = mBookmarker.getBookmark(mUri);
162 if (bookmark != null) {
163 showResumeDialog(movieActivity, bookmark);
164 } else {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800165 startVideo();
Owen Lin540e4692011-09-14 19:49:03 +0800166 }
Owen Linf9a0a432011-08-17 22:07:43 +0800167 }
168 }
169
Chih-Chung Chang81760122011-09-27 16:50:16 +0800170 private void showSystemUi(boolean visible) {
Chih-Chung Chang5383c572011-11-09 20:57:53 +0800171 int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
172 View.SYSTEM_UI_FLAG_LOW_PROFILE;
Chih-Chung Chang81760122011-09-27 16:50:16 +0800173 mVideoView.setSystemUiVisibility(flag);
Ray Chen392a2922012-05-14 17:19:56 +0800174 if (visible) {
175 mActionBar.show();
176 } else {
177 mActionBar.hide();
178 }
Chih-Chung Chang81760122011-09-27 16:50:16 +0800179 }
180
Owen Lin540e4692011-09-14 19:49:03 +0800181 public void onSaveInstanceState(Bundle outState) {
182 outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
183 outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
184 }
185
Owen Linf9a0a432011-08-17 22:07:43 +0800186 private void showResumeDialog(Context context, final int bookmark) {
187 AlertDialog.Builder builder = new AlertDialog.Builder(context);
188 builder.setTitle(R.string.resume_playing_title);
189 builder.setMessage(String.format(
190 context.getString(R.string.resume_playing_message),
191 GalleryUtils.formatDuration(context, bookmark / 1000)));
192 builder.setOnCancelListener(new OnCancelListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800193 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800194 public void onCancel(DialogInterface dialog) {
195 onCompletion();
196 }
197 });
198 builder.setPositiveButton(
199 R.string.resume_playing_resume, new OnClickListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800200 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800201 public void onClick(DialogInterface dialog, int which) {
202 mVideoView.seekTo(bookmark);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800203 startVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800204 }
205 });
206 builder.setNegativeButton(
207 R.string.resume_playing_restart, new OnClickListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800208 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800209 public void onClick(DialogInterface dialog, int which) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800210 startVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800211 }
212 });
213 builder.show();
214 }
215
216 public void onPause() {
Owen Linf9a0a432011-08-17 22:07:43 +0800217 mHasPaused = true;
Owen Lin540e4692011-09-14 19:49:03 +0800218 mHandler.removeCallbacksAndMessages(null);
219 mVideoPosition = mVideoView.getCurrentPosition();
220 mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
221 mVideoView.suspend();
222 mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
Owen Linf9a0a432011-08-17 22:07:43 +0800223 }
224
225 public void onResume() {
226 if (mHasPaused) {
Owen Lin540e4692011-09-14 19:49:03 +0800227 mVideoView.seekTo(mVideoPosition);
228 mVideoView.resume();
229
230 // If we have slept for too long, pause the play
231 if (System.currentTimeMillis() > mResumeableTime) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800232 pauseVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800233 }
234 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800235 mHandler.post(mProgressChecker);
Owen Linf9a0a432011-08-17 22:07:43 +0800236 }
237
238 public void onDestroy() {
239 mVideoView.stopPlayback();
240 mAudioBecomingNoisyReceiver.unregister();
241 }
242
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800243 // This updates the time bar display (if necessary). It is called every
244 // second by mProgressChecker and also from places where the time bar needs
245 // to be updated immediately.
246 private int setProgress() {
247 if (mDragging || !mShowing) {
248 return 0;
249 }
250 int position = mVideoView.getCurrentPosition();
251 int duration = mVideoView.getDuration();
252 mController.setTimes(position, duration);
253 return position;
254 }
255
256 private void startVideo() {
257 // For streams that we expect to be slow to start up, show a
258 // progress spinner until playback starts.
259 String scheme = mUri.getScheme();
260 if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
261 mController.showLoading();
262 mHandler.removeCallbacks(mPlayingChecker);
263 mHandler.postDelayed(mPlayingChecker, 250);
264 } else {
265 mController.showPlaying();
266 }
267
268 mVideoView.start();
269 setProgress();
270 }
271
272 private void playVideo() {
273 mVideoView.start();
274 mController.showPlaying();
275 setProgress();
276 }
277
278 private void pauseVideo() {
279 mVideoView.pause();
280 mController.showPaused();
281 }
282
283 // Below are notifications from VideoView
284 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800285 public boolean onError(MediaPlayer player, int arg1, int arg2) {
286 mHandler.removeCallbacksAndMessages(null);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800287 // VideoView will show an error dialog if we return false, so no need
288 // to show more message.
289 mController.showErrorMessage("");
Owen Linf9a0a432011-08-17 22:07:43 +0800290 return false;
291 }
292
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800293 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800294 public void onCompletion(MediaPlayer mp) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800295 mController.showEnded();
Owen Linf9a0a432011-08-17 22:07:43 +0800296 onCompletion();
297 }
298
299 public void onCompletion() {
300 }
301
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800302 // Below are notifications from ControllerOverlay
303 @Override
304 public void onPlayPause() {
305 if (mVideoView.isPlaying()) {
306 pauseVideo();
307 } else {
308 playVideo();
309 }
310 }
311
312 @Override
313 public void onSeekStart() {
314 mDragging = true;
315 }
316
317 @Override
318 public void onSeekMove(int time) {
319 mVideoView.seekTo(time);
320 }
321
322 @Override
323 public void onSeekEnd(int time) {
324 mDragging = false;
325 mVideoView.seekTo(time);
326 setProgress();
327 }
328
329 @Override
330 public void onShown() {
331 mShowing = true;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800332 setProgress();
Ray Chen392a2922012-05-14 17:19:56 +0800333
334 // System UI is invisible by default until the flag is set by user interaction
335 // See VideoView's onSystemUiVisibilityChange listener for details.
336 if (mAllowShowingSystemUI) {
337 showSystemUi(true);
338 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800339 }
340
341 @Override
342 public void onHidden() {
343 mShowing = false;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800344 showSystemUi(false);
345 }
346
347 @Override
348 public void onReplay() {
349 startVideo();
350 }
351
Chih-Chung Changfc8c5032011-11-29 14:21:11 +0800352 // Below are key events passed from MovieActivity.
353 public boolean onKeyDown(int keyCode, KeyEvent event) {
354
355 // Some headsets will fire off 7-10 events on a single click
356 if (event.getRepeatCount() > 0) {
357 return isMediaKey(keyCode);
358 }
359
360 switch (keyCode) {
361 case KeyEvent.KEYCODE_HEADSETHOOK:
362 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
363 if (mVideoView.isPlaying()) {
364 pauseVideo();
365 } else {
366 playVideo();
367 }
368 return true;
369 case KeyEvent.KEYCODE_MEDIA_PAUSE:
370 if (mVideoView.isPlaying()) {
371 pauseVideo();
372 }
373 return true;
374 case KeyEvent.KEYCODE_MEDIA_PLAY:
375 if (!mVideoView.isPlaying()) {
376 playVideo();
377 }
378 return true;
379 case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
380 case KeyEvent.KEYCODE_MEDIA_NEXT:
381 // TODO: Handle next / previous accordingly, for now we're
382 // just consuming the events.
383 return true;
384 }
385 return false;
386 }
387
388 public boolean onKeyUp(int keyCode, KeyEvent event) {
389 return isMediaKey(keyCode);
390 }
391
392 private static boolean isMediaKey(int keyCode) {
393 return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
394 || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
395 || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
396 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
397 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
398 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
399 }
400
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800401 // We want to pause when the headset is unplugged.
Owen Linf9a0a432011-08-17 22:07:43 +0800402 private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
403
404 public void register() {
405 mContext.registerReceiver(this,
406 new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
407 }
408
409 public void unregister() {
410 mContext.unregisterReceiver(this);
411 }
412
413 @Override
414 public void onReceive(Context context, Intent intent) {
Owen Linaea077a2011-11-11 12:01:09 +0800415 if (mVideoView.isPlaying()) pauseVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800416 }
417 }
418}
419
420class Bookmarker {
421 private static final String TAG = "Bookmarker";
422
423 private static final String BOOKMARK_CACHE_FILE = "bookmark";
424 private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
425 private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
426 private static final int BOOKMARK_CACHE_VERSION = 1;
427
428 private static final int HALF_MINUTE = 30 * 1000;
429 private static final int TWO_MINUTES = 4 * HALF_MINUTE;
430
431 private final Context mContext;
432
433 public Bookmarker(Context context) {
434 mContext = context;
435 }
436
437 public void setBookmark(Uri uri, int bookmark, int duration) {
438 try {
439 BlobCache cache = CacheManager.getCache(mContext,
440 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
441 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
442
443 ByteArrayOutputStream bos = new ByteArrayOutputStream();
444 DataOutputStream dos = new DataOutputStream(bos);
445 dos.writeUTF(uri.toString());
446 dos.writeInt(bookmark);
447 dos.writeInt(duration);
448 dos.flush();
449 cache.insert(uri.hashCode(), bos.toByteArray());
450 } catch (Throwable t) {
451 Log.w(TAG, "setBookmark failed", t);
452 }
453 }
454
455 public Integer getBookmark(Uri uri) {
456 try {
457 BlobCache cache = CacheManager.getCache(mContext,
458 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
459 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
460
461 byte[] data = cache.lookup(uri.hashCode());
462 if (data == null) return null;
463
464 DataInputStream dis = new DataInputStream(
465 new ByteArrayInputStream(data));
466
467 String uriString = dis.readUTF(dis);
468 int bookmark = dis.readInt();
469 int duration = dis.readInt();
470
471 if (!uriString.equals(uri.toString())) {
472 return null;
473 }
474
475 if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
476 || (bookmark > (duration - HALF_MINUTE))) {
477 return null;
478 }
479 return Integer.valueOf(bookmark);
480 } catch (Throwable t) {
481 Log.w(TAG, "getBookmark failed", t);
482 }
483 return null;
484 }
485}