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