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