MoviePlayer.java revision aea077a5767d28fcdade440825239adfbfb8f45a
1/* 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 19import 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; 31import android.os.Bundle; 32import android.os.Handler; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.ViewGroup; 36import android.widget.VideoView; 37 38import com.android.gallery3d.R; 39import com.android.gallery3d.common.BlobCache; 40import com.android.gallery3d.util.CacheManager; 41import com.android.gallery3d.util.GalleryUtils; 42 43import java.io.ByteArrayInputStream; 44import java.io.ByteArrayOutputStream; 45import java.io.DataInputStream; 46import java.io.DataOutputStream; 47 48public class MoviePlayer implements 49 MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, 50 ControllerOverlay.Listener { 51 @SuppressWarnings("unused") 52 private static final String TAG = "MoviePlayer"; 53 54 private static final String KEY_VIDEO_POSITION = "video-position"; 55 private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout"; 56 57 // Copied from MediaPlaybackService in the Music Player app. 58 private static final String SERVICECMD = "com.android.music.musicservicecommand"; 59 private static final String CMDNAME = "command"; 60 private static final String CMDPAUSE = "pause"; 61 62 // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing. 63 // Otherwise, we pause the player. 64 private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins 65 66 private Context mContext; 67 private final VideoView mVideoView; 68 private final Bookmarker mBookmarker; 69 private final Uri mUri; 70 private final Handler mHandler = new Handler(); 71 private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver; 72 private final ActionBar mActionBar; 73 private final ControllerOverlay mController; 74 75 private long mResumeableTime = Long.MAX_VALUE; 76 private int mVideoPosition = 0; 77 private boolean mHasPaused = false; 78 79 // If the time bar is being dragged. 80 private boolean mDragging; 81 82 // If the time bar is visible. 83 private boolean mShowing; 84 85 private final Runnable mPlayingChecker = new Runnable() { 86 @Override 87 public void run() { 88 if (mVideoView.isPlaying()) { 89 mController.showPlaying(); 90 } else { 91 mHandler.postDelayed(mPlayingChecker, 250); 92 } 93 } 94 }; 95 96 private final Runnable mProgressChecker = new Runnable() { 97 @Override 98 public void run() { 99 int pos = setProgress(); 100 mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000)); 101 } 102 }; 103 104 public MoviePlayer(View rootView, final MovieActivity movieActivity, Uri videoUri, 105 Bundle savedInstance, boolean canReplay) { 106 mContext = movieActivity.getApplicationContext(); 107 mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); 108 mBookmarker = new Bookmarker(movieActivity); 109 mActionBar = movieActivity.getActionBar(); 110 mUri = videoUri; 111 112 mController = new MovieControllerOverlay(mContext); 113 ((ViewGroup)rootView).addView(mController.getView()); 114 mController.setListener(this); 115 mController.setCanReplay(canReplay); 116 117 mVideoView.setOnErrorListener(this); 118 mVideoView.setOnCompletionListener(this); 119 mVideoView.setVideoURI(mUri); 120 mVideoView.setOnTouchListener(new View.OnTouchListener() { 121 public boolean onTouch(View v, MotionEvent event) { 122 mController.show(); 123 return true; 124 } 125 }); 126 127 // When the user touches the screen or uses some hard key, the framework 128 // will change system ui visibility from invisible to visible. We show 129 // the media control at this point. 130 mVideoView.setOnSystemUiVisibilityChangeListener( 131 new View.OnSystemUiVisibilityChangeListener() { 132 public void onSystemUiVisibilityChange(int visibility) { 133 if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { 134 mController.show(); 135 } 136 } 137 }); 138 139 mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); 140 mAudioBecomingNoisyReceiver.register(); 141 142 Intent i = new Intent(SERVICECMD); 143 i.putExtra(CMDNAME, CMDPAUSE); 144 movieActivity.sendBroadcast(i); 145 146 if (savedInstance != null) { // this is a resumed activity 147 mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0); 148 mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE); 149 mVideoView.start(); 150 mVideoView.suspend(); 151 mHasPaused = true; 152 } else { 153 final Integer bookmark = mBookmarker.getBookmark(mUri); 154 if (bookmark != null) { 155 showResumeDialog(movieActivity, bookmark); 156 } else { 157 startVideo(); 158 } 159 } 160 } 161 162 private void showSystemUi(boolean visible) { 163 int flag = visible ? 0 : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | 164 View.SYSTEM_UI_FLAG_LOW_PROFILE; 165 mVideoView.setSystemUiVisibility(flag); 166 } 167 168 public void onSaveInstanceState(Bundle outState) { 169 outState.putInt(KEY_VIDEO_POSITION, mVideoPosition); 170 outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime); 171 } 172 173 private void showResumeDialog(Context context, final int bookmark) { 174 AlertDialog.Builder builder = new AlertDialog.Builder(context); 175 builder.setTitle(R.string.resume_playing_title); 176 builder.setMessage(String.format( 177 context.getString(R.string.resume_playing_message), 178 GalleryUtils.formatDuration(context, bookmark / 1000))); 179 builder.setOnCancelListener(new OnCancelListener() { 180 @Override 181 public void onCancel(DialogInterface dialog) { 182 onCompletion(); 183 } 184 }); 185 builder.setPositiveButton( 186 R.string.resume_playing_resume, new OnClickListener() { 187 @Override 188 public void onClick(DialogInterface dialog, int which) { 189 mVideoView.seekTo(bookmark); 190 startVideo(); 191 } 192 }); 193 builder.setNegativeButton( 194 R.string.resume_playing_restart, new OnClickListener() { 195 @Override 196 public void onClick(DialogInterface dialog, int which) { 197 startVideo(); 198 } 199 }); 200 builder.show(); 201 } 202 203 public void onPause() { 204 mHasPaused = true; 205 mHandler.removeCallbacksAndMessages(null); 206 mVideoPosition = mVideoView.getCurrentPosition(); 207 mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration()); 208 mVideoView.suspend(); 209 mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT; 210 } 211 212 public void onResume() { 213 if (mHasPaused) { 214 mVideoView.seekTo(mVideoPosition); 215 mVideoView.resume(); 216 217 // If we have slept for too long, pause the play 218 if (System.currentTimeMillis() > mResumeableTime) { 219 pauseVideo(); 220 } 221 } 222 mHandler.post(mProgressChecker); 223 } 224 225 public void onDestroy() { 226 mVideoView.stopPlayback(); 227 mAudioBecomingNoisyReceiver.unregister(); 228 } 229 230 // This updates the time bar display (if necessary). It is called every 231 // second by mProgressChecker and also from places where the time bar needs 232 // to be updated immediately. 233 private int setProgress() { 234 if (mDragging || !mShowing) { 235 return 0; 236 } 237 int position = mVideoView.getCurrentPosition(); 238 int duration = mVideoView.getDuration(); 239 mController.setTimes(position, duration); 240 return position; 241 } 242 243 private void startVideo() { 244 // For streams that we expect to be slow to start up, show a 245 // progress spinner until playback starts. 246 String scheme = mUri.getScheme(); 247 if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) { 248 mController.showLoading(); 249 mHandler.removeCallbacks(mPlayingChecker); 250 mHandler.postDelayed(mPlayingChecker, 250); 251 } else { 252 mController.showPlaying(); 253 } 254 255 mVideoView.start(); 256 setProgress(); 257 } 258 259 private void playVideo() { 260 mVideoView.start(); 261 mController.showPlaying(); 262 setProgress(); 263 } 264 265 private void pauseVideo() { 266 mVideoView.pause(); 267 mController.showPaused(); 268 } 269 270 // Below are notifications from VideoView 271 @Override 272 public boolean onError(MediaPlayer player, int arg1, int arg2) { 273 mHandler.removeCallbacksAndMessages(null); 274 // VideoView will show an error dialog if we return false, so no need 275 // to show more message. 276 mController.showErrorMessage(""); 277 return false; 278 } 279 280 @Override 281 public void onCompletion(MediaPlayer mp) { 282 mController.showEnded(); 283 onCompletion(); 284 } 285 286 public void onCompletion() { 287 } 288 289 // Below are notifications from ControllerOverlay 290 @Override 291 public void onPlayPause() { 292 if (mVideoView.isPlaying()) { 293 pauseVideo(); 294 } else { 295 playVideo(); 296 } 297 } 298 299 @Override 300 public void onSeekStart() { 301 mDragging = true; 302 } 303 304 @Override 305 public void onSeekMove(int time) { 306 mVideoView.seekTo(time); 307 } 308 309 @Override 310 public void onSeekEnd(int time) { 311 mDragging = false; 312 mVideoView.seekTo(time); 313 setProgress(); 314 } 315 316 @Override 317 public void onShown() { 318 mShowing = true; 319 mActionBar.show(); 320 showSystemUi(true); 321 setProgress(); 322 } 323 324 @Override 325 public void onHidden() { 326 mShowing = false; 327 mActionBar.hide(); 328 showSystemUi(false); 329 } 330 331 @Override 332 public void onReplay() { 333 startVideo(); 334 } 335 336 // We want to pause when the headset is unplugged. 337 private class AudioBecomingNoisyReceiver extends BroadcastReceiver { 338 339 public void register() { 340 mContext.registerReceiver(this, 341 new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); 342 } 343 344 public void unregister() { 345 mContext.unregisterReceiver(this); 346 } 347 348 @Override 349 public void onReceive(Context context, Intent intent) { 350 if (mVideoView.isPlaying()) pauseVideo(); 351 } 352 } 353} 354 355class Bookmarker { 356 private static final String TAG = "Bookmarker"; 357 358 private static final String BOOKMARK_CACHE_FILE = "bookmark"; 359 private static final int BOOKMARK_CACHE_MAX_ENTRIES = 100; 360 private static final int BOOKMARK_CACHE_MAX_BYTES = 10 * 1024; 361 private static final int BOOKMARK_CACHE_VERSION = 1; 362 363 private static final int HALF_MINUTE = 30 * 1000; 364 private static final int TWO_MINUTES = 4 * HALF_MINUTE; 365 366 private final Context mContext; 367 368 public Bookmarker(Context context) { 369 mContext = context; 370 } 371 372 public void setBookmark(Uri uri, int bookmark, int duration) { 373 try { 374 BlobCache cache = CacheManager.getCache(mContext, 375 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, 376 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); 377 378 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 379 DataOutputStream dos = new DataOutputStream(bos); 380 dos.writeUTF(uri.toString()); 381 dos.writeInt(bookmark); 382 dos.writeInt(duration); 383 dos.flush(); 384 cache.insert(uri.hashCode(), bos.toByteArray()); 385 } catch (Throwable t) { 386 Log.w(TAG, "setBookmark failed", t); 387 } 388 } 389 390 public Integer getBookmark(Uri uri) { 391 try { 392 BlobCache cache = CacheManager.getCache(mContext, 393 BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, 394 BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); 395 396 byte[] data = cache.lookup(uri.hashCode()); 397 if (data == null) return null; 398 399 DataInputStream dis = new DataInputStream( 400 new ByteArrayInputStream(data)); 401 402 String uriString = dis.readUTF(dis); 403 int bookmark = dis.readInt(); 404 int duration = dis.readInt(); 405 406 if (!uriString.equals(uri.toString())) { 407 return null; 408 } 409 410 if ((bookmark < HALF_MINUTE) || (duration < TWO_MINUTES) 411 || (bookmark > (duration - HALF_MINUTE))) { 412 return null; 413 } 414 return Integer.valueOf(bookmark); 415 } catch (Throwable t) { 416 Log.w(TAG, "getBookmark failed", t); 417 } 418 return null; 419 } 420} 421