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