blob: f6bd3672522fa3c82e8728435cb75110cbc6301c [file] [log] [blame]
/*
* 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.media.audiofx.AudioEffect;
import android.media.audiofx.Virtualizer;
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 String VIRTUALIZE_EXTRA = "virtualize";
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 Virtualizer mVirtualizer;
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);
Intent ai = movieActivity.getIntent();
boolean virtualize = ai.getBooleanExtra(VIRTUALIZE_EXTRA, false);
if (virtualize) {
int session = mVideoView.getAudioSessionId();
if (session != 0) {
mVirtualizer = new Virtualizer(0, session);
mVirtualizer.setEnabled(true);
} else {
Log.w(TAG, "no audio session to virtualize");
}
}
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() {
if (mVirtualizer != null) {
mVirtualizer.release();
mVirtualizer = null;
}
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;
}
}