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