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