blob: 565d2b239c468a7fbfca972e6ba1495409be2ff7 [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;
Ray Chen24525c22012-05-22 17:34:29 +080079 private int mLastSystemUiVis = 0;
Owen Linf9a0a432011-08-17 22:07:43 +080080
Chih-Chung Chang209a9162011-10-14 16:09:09 +080081 // If the time bar is being dragged.
82 private boolean mDragging;
83
84 // If the time bar is visible.
85 private boolean mShowing;
86
Ray Chen392a2922012-05-14 17:19:56 +080087 // Control when system UI can be shown
88 private boolean mAllowShowingSystemUI;
89
Owen Linf9a0a432011-08-17 22:07:43 +080090 private final Runnable mPlayingChecker = new Runnable() {
Owen Lin540e4692011-09-14 19:49:03 +080091 @Override
Owen Linf9a0a432011-08-17 22:07:43 +080092 public void run() {
93 if (mVideoView.isPlaying()) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +080094 mController.showPlaying();
Owen Linf9a0a432011-08-17 22:07:43 +080095 } else {
96 mHandler.postDelayed(mPlayingChecker, 250);
97 }
98 }
99 };
100
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800101 private final Runnable mProgressChecker = new Runnable() {
102 @Override
103 public void run() {
104 int pos = setProgress();
105 mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
106 }
107 };
108
Owen Lin540e4692011-09-14 19:49:03 +0800109 public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri,
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800110 Bundle savedInstance, boolean canReplay) {
Owen Linf9a0a432011-08-17 22:07:43 +0800111 mContext = movieActivity.getApplicationContext();
112 mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
Owen Linf9a0a432011-08-17 22:07:43 +0800113 mBookmarker = new Bookmarker(movieActivity);
114 mActionBar = movieActivity.getActionBar();
115 mUri = videoUri;
116
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800117 mController = new MovieControllerOverlay(mContext);
118 ((ViewGroup)rootView).addView(mController.getView());
119 mController.setListener(this);
120 mController.setCanReplay(canReplay);
Owen Linf9a0a432011-08-17 22:07:43 +0800121
122 mVideoView.setOnErrorListener(this);
123 mVideoView.setOnCompletionListener(this);
124 mVideoView.setVideoURI(mUri);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800125 mVideoView.setOnTouchListener(new View.OnTouchListener() {
126 public boolean onTouch(View v, MotionEvent event) {
127 mController.show();
128 return true;
Owen Linf9a0a432011-08-17 22:07:43 +0800129 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800130 });
Chih-Chung Chang81760122011-09-27 16:50:16 +0800131
132 // When the user touches the screen or uses some hard key, the framework
133 // will change system ui visibility from invisible to visible. We show
Ray Chen392a2922012-05-14 17:19:56 +0800134 // the media control and enable system UI (e.g. ActionBar) to be visible at this point
Chih-Chung Chang81760122011-09-27 16:50:16 +0800135 mVideoView.setOnSystemUiVisibilityChangeListener(
136 new View.OnSystemUiVisibilityChangeListener() {
137 public void onSystemUiVisibilityChange(int visibility) {
Ray Chen24525c22012-05-22 17:34:29 +0800138 int diff = mLastSystemUiVis ^ visibility;
139 mLastSystemUiVis = visibility;
140 if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
141 && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
Ray Chen392a2922012-05-14 17:19:56 +0800142 mAllowShowingSystemUI = true;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800143 mController.show();
Chih-Chung Chang81760122011-09-27 16:50:16 +0800144 }
145 }
146 });
147
Ray Chen392a2922012-05-14 17:19:56 +0800148 // Hide system UI by default
149 showSystemUi(false);
150
Owen Linf9a0a432011-08-17 22:07:43 +0800151 mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
152 mAudioBecomingNoisyReceiver.register();
153
Owen Linf9a0a432011-08-17 22:07:43 +0800154 Intent i = new Intent(SERVICECMD);
155 i.putExtra(CMDNAME, CMDPAUSE);
156 movieActivity.sendBroadcast(i);
157
Owen Lin540e4692011-09-14 19:49:03 +0800158 if (savedInstance != null) { // this is a resumed activity
159 mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
160 mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
Owen Linf9a0a432011-08-17 22:07:43 +0800161 mVideoView.start();
Owen Lin540e4692011-09-14 19:49:03 +0800162 mVideoView.suspend();
163 mHasPaused = true;
164 } else {
165 final Integer bookmark = mBookmarker.getBookmark(mUri);
166 if (bookmark != null) {
167 showResumeDialog(movieActivity, bookmark);
168 } else {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800169 startVideo();
Owen Lin540e4692011-09-14 19:49:03 +0800170 }
Owen Linf9a0a432011-08-17 22:07:43 +0800171 }
172 }
173
Chih-Chung Chang81760122011-09-27 16:50:16 +0800174 private void showSystemUi(boolean visible) {
Ray Chen24525c22012-05-22 17:34:29 +0800175 int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
176 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
177 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
178 if (!visible) {
179 flag |= View.SYSTEM_UI_FLAG_LOW_PROFILE | View.SYSTEM_UI_FLAG_FULLSCREEN
180 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
Ray Chen392a2922012-05-14 17:19:56 +0800181 }
Ray Chen24525c22012-05-22 17:34:29 +0800182 mVideoView.setSystemUiVisibility(flag);
Chih-Chung Chang81760122011-09-27 16:50:16 +0800183 }
184
Owen Lin540e4692011-09-14 19:49:03 +0800185 public void onSaveInstanceState(Bundle outState) {
186 outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
187 outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
188 }
189
Owen Linf9a0a432011-08-17 22:07:43 +0800190 private void showResumeDialog(Context context, final int bookmark) {
191 AlertDialog.Builder builder = new AlertDialog.Builder(context);
192 builder.setTitle(R.string.resume_playing_title);
193 builder.setMessage(String.format(
194 context.getString(R.string.resume_playing_message),
195 GalleryUtils.formatDuration(context, bookmark / 1000)));
196 builder.setOnCancelListener(new OnCancelListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800197 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800198 public void onCancel(DialogInterface dialog) {
199 onCompletion();
200 }
201 });
202 builder.setPositiveButton(
203 R.string.resume_playing_resume, new OnClickListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800204 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800205 public void onClick(DialogInterface dialog, int which) {
206 mVideoView.seekTo(bookmark);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800207 startVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800208 }
209 });
210 builder.setNegativeButton(
211 R.string.resume_playing_restart, new OnClickListener() {
Owen Lin540e4692011-09-14 19:49:03 +0800212 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800213 public void onClick(DialogInterface dialog, int which) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800214 startVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800215 }
216 });
217 builder.show();
218 }
219
220 public void onPause() {
Owen Linf9a0a432011-08-17 22:07:43 +0800221 mHasPaused = true;
Owen Lin540e4692011-09-14 19:49:03 +0800222 mHandler.removeCallbacksAndMessages(null);
223 mVideoPosition = mVideoView.getCurrentPosition();
224 mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
225 mVideoView.suspend();
226 mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
Owen Linf9a0a432011-08-17 22:07:43 +0800227 }
228
229 public void onResume() {
230 if (mHasPaused) {
Owen Lin540e4692011-09-14 19:49:03 +0800231 mVideoView.seekTo(mVideoPosition);
232 mVideoView.resume();
233
234 // If we have slept for too long, pause the play
235 if (System.currentTimeMillis() > mResumeableTime) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800236 pauseVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800237 }
238 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800239 mHandler.post(mProgressChecker);
Owen Linf9a0a432011-08-17 22:07:43 +0800240 }
241
242 public void onDestroy() {
243 mVideoView.stopPlayback();
244 mAudioBecomingNoisyReceiver.unregister();
245 }
246
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800247 // This updates the time bar display (if necessary). It is called every
248 // second by mProgressChecker and also from places where the time bar needs
249 // to be updated immediately.
250 private int setProgress() {
251 if (mDragging || !mShowing) {
252 return 0;
253 }
254 int position = mVideoView.getCurrentPosition();
255 int duration = mVideoView.getDuration();
256 mController.setTimes(position, duration);
257 return position;
258 }
259
260 private void startVideo() {
261 // For streams that we expect to be slow to start up, show a
262 // progress spinner until playback starts.
263 String scheme = mUri.getScheme();
264 if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
265 mController.showLoading();
266 mHandler.removeCallbacks(mPlayingChecker);
267 mHandler.postDelayed(mPlayingChecker, 250);
268 } else {
269 mController.showPlaying();
270 }
271
272 mVideoView.start();
273 setProgress();
274 }
275
276 private void playVideo() {
277 mVideoView.start();
278 mController.showPlaying();
279 setProgress();
280 }
281
282 private void pauseVideo() {
283 mVideoView.pause();
284 mController.showPaused();
285 }
286
287 // Below are notifications from VideoView
288 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800289 public boolean onError(MediaPlayer player, int arg1, int arg2) {
290 mHandler.removeCallbacksAndMessages(null);
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800291 // VideoView will show an error dialog if we return false, so no need
292 // to show more message.
293 mController.showErrorMessage("");
Owen Linf9a0a432011-08-17 22:07:43 +0800294 return false;
295 }
296
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800297 @Override
Owen Linf9a0a432011-08-17 22:07:43 +0800298 public void onCompletion(MediaPlayer mp) {
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800299 mController.showEnded();
Owen Linf9a0a432011-08-17 22:07:43 +0800300 onCompletion();
301 }
302
303 public void onCompletion() {
304 }
305
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800306 // Below are notifications from ControllerOverlay
307 @Override
308 public void onPlayPause() {
309 if (mVideoView.isPlaying()) {
310 pauseVideo();
311 } else {
312 playVideo();
313 }
314 }
315
316 @Override
317 public void onSeekStart() {
318 mDragging = true;
319 }
320
321 @Override
322 public void onSeekMove(int time) {
323 mVideoView.seekTo(time);
324 }
325
326 @Override
327 public void onSeekEnd(int time) {
328 mDragging = false;
329 mVideoView.seekTo(time);
330 setProgress();
331 }
332
333 @Override
334 public void onShown() {
335 mShowing = true;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800336 setProgress();
Ray Chen392a2922012-05-14 17:19:56 +0800337
338 // System UI is invisible by default until the flag is set by user interaction
339 // See VideoView's onSystemUiVisibilityChange listener for details.
340 if (mAllowShowingSystemUI) {
341 showSystemUi(true);
342 }
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800343 }
344
345 @Override
346 public void onHidden() {
347 mShowing = false;
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800348 showSystemUi(false);
349 }
350
351 @Override
352 public void onReplay() {
353 startVideo();
354 }
355
Chih-Chung Changfc8c5032011-11-29 14:21:11 +0800356 // Below are key events passed from MovieActivity.
357 public boolean onKeyDown(int keyCode, KeyEvent event) {
358
359 // Some headsets will fire off 7-10 events on a single click
360 if (event.getRepeatCount() > 0) {
361 return isMediaKey(keyCode);
362 }
363
364 switch (keyCode) {
365 case KeyEvent.KEYCODE_HEADSETHOOK:
366 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
367 if (mVideoView.isPlaying()) {
368 pauseVideo();
369 } else {
370 playVideo();
371 }
372 return true;
373 case KeyEvent.KEYCODE_MEDIA_PAUSE:
374 if (mVideoView.isPlaying()) {
375 pauseVideo();
376 }
377 return true;
378 case KeyEvent.KEYCODE_MEDIA_PLAY:
379 if (!mVideoView.isPlaying()) {
380 playVideo();
381 }
382 return true;
383 case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
384 case KeyEvent.KEYCODE_MEDIA_NEXT:
385 // TODO: Handle next / previous accordingly, for now we're
386 // just consuming the events.
387 return true;
388 }
389 return false;
390 }
391
392 public boolean onKeyUp(int keyCode, KeyEvent event) {
393 return isMediaKey(keyCode);
394 }
395
396 private static boolean isMediaKey(int keyCode) {
397 return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
398 || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
399 || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
400 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
401 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
402 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
403 }
404
Chih-Chung Chang209a9162011-10-14 16:09:09 +0800405 // We want to pause when the headset is unplugged.
Owen Linf9a0a432011-08-17 22:07:43 +0800406 private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
407
408 public void register() {
409 mContext.registerReceiver(this,
410 new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
411 }
412
413 public void unregister() {
414 mContext.unregisterReceiver(this);
415 }
416
417 @Override
418 public void onReceive(Context context, Intent intent) {
Owen Linaea077a2011-11-11 12:01:09 +0800419 if (mVideoView.isPlaying()) pauseVideo();
Owen Linf9a0a432011-08-17 22:07:43 +0800420 }
421 }
422}
423
424class Bookmarker {
425 private static final String TAG = "Bookmarker";
426
427 private static final String BOOKMARK_CACHE_FILE = "bookmark";
428 private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
429 private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
430 private static final int BOOKMARK_CACHE_VERSION = 1;
431
432 private static final int HALF_MINUTE = 30 * 1000;
433 private static final int TWO_MINUTES = 4 * HALF_MINUTE;
434
435 private final Context mContext;
436
437 public Bookmarker(Context context) {
438 mContext = context;
439 }
440
441 public void setBookmark(Uri uri, int bookmark, int duration) {
442 try {
443 BlobCache cache = CacheManager.getCache(mContext,
444 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
445 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
446
447 ByteArrayOutputStream bos = new ByteArrayOutputStream();
448 DataOutputStream dos = new DataOutputStream(bos);
449 dos.writeUTF(uri.toString());
450 dos.writeInt(bookmark);
451 dos.writeInt(duration);
452 dos.flush();
453 cache.insert(uri.hashCode(), bos.toByteArray());
454 } catch (Throwable t) {
455 Log.w(TAG, "setBookmark failed", t);
456 }
457 }
458
459 public Integer getBookmark(Uri uri) {
460 try {
461 BlobCache cache = CacheManager.getCache(mContext,
462 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
463 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
464
465 byte[] data = cache.lookup(uri.hashCode());
466 if (data == null) return null;
467
468 DataInputStream dis = new DataInputStream(
469 new ByteArrayInputStream(data));
470
471 String uriString = dis.readUTF(dis);
472 int bookmark = dis.readInt();
473 int duration = dis.readInt();
474
475 if (!uriString.equals(uri.toString())) {
476 return null;
477 }
478
479 if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
480 || (bookmark > (duration - HALF_MINUTE))) {
481 return null;
482 }
483 return Integer.valueOf(bookmark);
484 } catch (Throwable t) {
485 Log.w(TAG, "getBookmark failed", t);
486 }
487 return null;
488 }
489}