diff options
Diffstat (limited to 'src/com/android/gallery3d/app/MoviePlayer.java')
-rw-r--r-- | src/com/android/gallery3d/app/MoviePlayer.java | 525 |
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; + } +} |