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