• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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