summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/app/MoviePlayer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/app/MoviePlayer.java')
-rw-r--r--src/com/android/gallery3d/app/MoviePlayer.java525
1 files changed, 525 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 000000000..ce9183483
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.app;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+ MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
+ ControllerOverlay.Listener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoviePlayer";
+
+ private static final String KEY_VIDEO_POSITION = "video-position";
+ private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
+
+ // These are constants in KeyEvent, appearing on API level 11.
+ private static final int KEYCODE_MEDIA_PLAY = 126;
+ private static final int KEYCODE_MEDIA_PAUSE = 127;
+
+ // Copied from MediaPlaybackService in the Music Player app.
+ private static final String SERVICECMD = "com.android.music.musicservicecommand";
+ private static final String CMDNAME = "command";
+ private static final String CMDPAUSE = "pause";
+
+ private static final long BLACK_TIMEOUT = 500;
+
+ // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
+ // Otherwise, we pause the player.
+ private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
+
+ private Context mContext;
+ private final VideoView mVideoView;
+ private final View mRootView;
+ private final Bookmarker mBookmarker;
+ private final Uri mUri;
+ private final Handler mHandler = new Handler();
+ private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+ private final MovieControllerOverlay mController;
+
+ private long mResumeableTime = Long.MAX_VALUE;
+ private int mVideoPosition = 0;
+ private boolean mHasPaused = false;
+ private int mLastSystemUiVis = 0;
+
+ // If the time bar is being dragged.
+ private boolean mDragging;
+
+ // If the time bar is visible.
+ private boolean mShowing;
+
+ private final Runnable mPlayingChecker = new Runnable() {
+ @Override
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mController.showPlaying();
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ private final Runnable mProgressChecker = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
+ }
+ };
+
+ public MoviePlayer(View rootView, final MovieActivity movieActivity,
+ Uri videoUri, Bundle savedInstance, boolean canReplay) {
+ mContext = movieActivity.getApplicationContext();
+ mRootView = rootView;
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+ mBookmarker = new Bookmarker(movieActivity);
+ mUri = videoUri;
+
+ mController = new MovieControllerOverlay(mContext);
+ ((ViewGroup)rootView).addView(mController.getView());
+ mController.setListener(this);
+ mController.setCanReplay(canReplay);
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+ mVideoView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ mController.show();
+ return true;
+ }
+ });
+ mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer player) {
+ if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) {
+ mController.setSeekable(false);
+ } else {
+ mController.setSeekable(true);
+ }
+ setProgress();
+ }
+ });
+
+ // The SurfaceView is transparent before drawing the first frame.
+ // This makes the UI flashing when open a video. (black -> old screen
+ // -> video) However, we have no way to know the timing of the first
+ // frame. So, we hide the VideoView for a while to make sure the
+ // video has been drawn on it.
+ mVideoView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mVideoView.setVisibility(View.VISIBLE);
+ }
+ }, BLACK_TIMEOUT);
+
+ setOnSystemUiVisibilityChangeListener();
+ // Hide system UI by default
+ showSystemUi(false);
+
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+ mAudioBecomingNoisyReceiver.register();
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ movieActivity.sendBroadcast(i);
+
+ if (savedInstance != null) { // this is a resumed activity
+ mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
+ mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
+ mVideoView.start();
+ mVideoView.suspend();
+ mHasPaused = true;
+ } else {
+ final Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ showResumeDialog(movieActivity, bookmark);
+ } else {
+ startVideo();
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setOnSystemUiVisibilityChangeListener() {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+
+ // When the user touches the screen or uses some hard key, the framework
+ // will change system ui visibility from invisible to visible. We show
+ // the media control and enable system UI (e.g. ActionBar) to be visible at this point
+ mVideoView.setOnSystemUiVisibilityChangeListener(
+ new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ int diff = mLastSystemUiVis ^ visibility;
+ mLastSystemUiVis = visibility;
+ if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+ && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+ mController.show();
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void showSystemUi(boolean visible) {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return;
+
+ int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+ if (!visible) {
+ // We used the deprecated "STATUS_BAR_HIDDEN" for unbundling
+ flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ }
+ mVideoView.setSystemUiVisibility(flag);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+ outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
+ }
+
+ private void showResumeDialog(Context context, final int bookmark) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.resume_playing_title);
+ builder.setMessage(String.format(
+ context.getString(R.string.resume_playing_message),
+ GalleryUtils.formatDuration(context, bookmark / 1000)));
+ builder.setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCompletion();
+ }
+ });
+ builder.setPositiveButton(
+ R.string.resume_playing_resume, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ startVideo();
+ }
+ });
+ builder.setNegativeButton(
+ R.string.resume_playing_restart, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startVideo();
+ }
+ });
+ builder.show();
+ }
+
+ public void onPause() {
+ mHasPaused = true;
+ mHandler.removeCallbacksAndMessages(null);
+ mVideoPosition = mVideoView.getCurrentPosition();
+ mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
+ mVideoView.suspend();
+ mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
+ }
+
+ public void onResume() {
+ if (mHasPaused) {
+ mVideoView.seekTo(mVideoPosition);
+ mVideoView.resume();
+
+ // If we have slept for too long, pause the play
+ if (System.currentTimeMillis() > mResumeableTime) {
+ pauseVideo();
+ }
+ }
+ mHandler.post(mProgressChecker);
+ }
+
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+
+ // This updates the time bar display (if necessary). It is called every
+ // second by mProgressChecker and also from places where the time bar needs
+ // to be updated immediately.
+ private int setProgress() {
+ if (mDragging || !mShowing) {
+ return 0;
+ }
+ int position = mVideoView.getCurrentPosition();
+ int duration = mVideoView.getDuration();
+ mController.setTimes(position, duration, 0, 0);
+ return position;
+ }
+
+ private void startVideo() {
+ // For streams that we expect to be slow to start up, show a
+ // progress spinner until playback starts.
+ String scheme = mUri.getScheme();
+ if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) {
+ mController.showLoading();
+ mHandler.removeCallbacks(mPlayingChecker);
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mController.showPlaying();
+ mController.hide();
+ }
+
+ mVideoView.start();
+ setProgress();
+ }
+
+ private void playVideo() {
+ mVideoView.start();
+ mController.showPlaying();
+ setProgress();
+ }
+
+ private void pauseVideo() {
+ mVideoView.pause();
+ mController.showPaused();
+ }
+
+ // Below are notifications from VideoView
+ @Override
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ // VideoView will show an error dialog if we return false, so no need
+ // to show more message.
+ mController.showErrorMessage("");
+ return false;
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mController.showEnded();
+ onCompletion();
+ }
+
+ public void onCompletion() {
+ }
+
+ // Below are notifications from ControllerOverlay
+ @Override
+ public void onPlayPause() {
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ }
+
+ @Override
+ public void onSeekStart() {
+ mDragging = true;
+ }
+
+ @Override
+ public void onSeekMove(int time) {
+ mVideoView.seekTo(time);
+ }
+
+ @Override
+ public void onSeekEnd(int time, int start, int end) {
+ mDragging = false;
+ mVideoView.seekTo(time);
+ setProgress();
+ }
+
+ @Override
+ public void onShown() {
+ mShowing = true;
+ setProgress();
+ showSystemUi(true);
+ }
+
+ @Override
+ public void onHidden() {
+ mShowing = false;
+ showSystemUi(false);
+ }
+
+ @Override
+ public void onReplay() {
+ startVideo();
+ }
+
+ // Below are key events passed from MovieActivity.
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ // Some headsets will fire off 7-10 events on a single click
+ if (event.getRepeatCount() > 0) {
+ return isMediaKey(keyCode);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PLAY:
+ if (!mVideoView.isPlaying()) {
+ playVideo();
+ }
+ return true;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ // TODO: Handle next / previous accordingly, for now we're
+ // just consuming the events.
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return isMediaKey(keyCode);
+ }
+
+ private static boolean isMediaKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
+ || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
+ }
+
+ // We want to pause when the headset is unplugged.
+ private class AudioBecomingNoisyReceiver extends BroadcastReceiver {
+
+ public void register() {
+ mContext.registerReceiver(this,
+ new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
+ }
+
+ public void unregister() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mVideoView.isPlaying()) pauseVideo();
+ }
+ }
+}
+
+class Bookmarker {
+ private static final String TAG = "Bookmarker";
+
+ private static final String BOOKMARK_CACHE_FILE = "bookmark";
+ private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100;
+ private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024;
+ private static final int BOOKMARK_CACHE_VERSION = 1;
+
+ private static final int HALF_MINUTE = 30 * 1000;
+ private static final int TWO_MINUTES = 4 * HALF_MINUTE;
+
+ private final Context mContext;
+
+ public Bookmarker(Context context) {
+ mContext = context;
+ }
+
+ public void setBookmark(Uri uri, int bookmark, int duration) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeUTF(uri.toString());
+ dos.writeInt(bookmark);
+ dos.writeInt(duration);
+ dos.flush();
+ cache.insert(uri.hashCode(), bos.toByteArray());
+ } catch (Throwable t) {
+ Log.w(TAG, "setBookmark failed", t);
+ }
+ }
+
+ public Integer getBookmark(Uri uri) {
+ try {
+ BlobCache cache = CacheManager.getCache(mContext,
+ BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES,
+ BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION);
+
+ byte[] data = cache.lookup(uri.hashCode());
+ if (data == null) return null;
+
+ DataInputStream dis = new DataInputStream(
+ new ByteArrayInputStream(data));
+
+ String uriString = DataInputStream.readUTF(dis);
+ int bookmark = dis.readInt();
+ int duration = dis.readInt();
+
+ if (!uriString.equals(uri.toString())) {
+ return null;
+ }
+
+ if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES)
+ || (bookmark > (duration - HALF_MINUTE))) {
+ return null;
+ }
+ return Integer.valueOf(bookmark);
+ } catch (Throwable t) {
+ Log.w(TAG, "getBookmark failed", t);
+ }
+ return null;
+ }
+}