Create our own media control - basic operation
[pub/Android/ownCloud.git] / src / com / owncloud / android / media / MediaControlView.java
1 /* ownCloud Android client application
2 *
3 * Copyright (C) 2012-2013 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19 package com.owncloud.android.media;
20
21 import android.content.Context;
22 import android.graphics.PixelFormat;
23 import android.media.AudioManager;
24 import android.media.MediaPlayer;
25 import android.os.Handler;
26 import android.os.Message;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.Gravity;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.View.OnLayoutChangeListener;
35 import android.view.View.OnTouchListener;
36 import android.view.ViewGroup;
37 import android.view.Window;
38 import android.view.WindowManager;
39 import android.view.accessibility.AccessibilityEvent;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 import android.widget.FrameLayout;
42 import android.widget.ImageButton;
43 import android.widget.MediaController.MediaPlayerControl;
44 import android.widget.ProgressBar;
45 import android.widget.SeekBar;
46 import android.widget.SeekBar.OnSeekBarChangeListener;
47 import android.widget.TextView;
48
49 import java.util.Formatter;
50 import java.util.Locale;
51
52 import com.owncloud.android.R;
53
54 /**
55 * View containing controls for a {@link MediaPlayer}.
56 *
57 * Holds buttons "play / pause", "rewind", "fast forward"
58 * and a progress slider.
59 *
60 * It synchronizes itself with the state of the
61 * {@link MediaPlayer}.
62 *
63 * @author David A. Velasco
64 */
65
66 public class MediaControlView extends FrameLayout implements /* OnLayoutChangeListener, */ OnTouchListener {
67
68 private static final String TAG = MediaControlView.class.getSimpleName();
69
70
71 private MediaPlayerControl mPlayer;
72 private Context mContext;
73 private View mAnchor;
74 private View mRoot;
75 private WindowManager mWindowManager;
76 //private Window mWindow;
77 private View mDecor;
78 private WindowManager.LayoutParams mDecorLayoutParams;
79 private ProgressBar mProgress;
80 private TextView mEndTime, mCurrentTime;
81 private boolean mShowing;
82 private boolean mDragging;
83 private static final int sDefaultTimeout = 3000;
84 private static final int FADE_OUT = 1;
85 private static final int SHOW_PROGRESS = 2;
86 private boolean mUseFastForward;
87 private boolean mFromXml;
88 private boolean mListenersSet;
89 private View.OnClickListener mNextListener, mPrevListener;
90 StringBuilder mFormatBuilder;
91 Formatter mFormatter;
92 private ImageButton mPauseButton;
93 private ImageButton mFfwdButton;
94 private ImageButton mRewButton;
95 private ImageButton mNextButton;
96 private ImageButton mPrevButton;
97
98 public MediaControlView(Context context, AttributeSet attrs) {
99 super(context, attrs);
100 //mRoot = this; // TODO review if this is adequate
101 mContext = context;
102 mUseFastForward = true;
103 mFromXml = true;
104
105 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
106
107 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
108 ViewGroup.LayoutParams.MATCH_PARENT,
109 ViewGroup.LayoutParams.MATCH_PARENT
110 );
111 //removeAllViews();
112 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
113 mRoot = inflate.inflate(R.layout.media_control, null);
114 initControllerView(mRoot);
115 addView(mRoot, frameParams);
116
117 setFocusable(true);
118 setFocusableInTouchMode(true);
119 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
120 requestFocus();
121 }
122
123 @Override
124 public void onFinishInflate() {
125 if (mRoot != null)
126 initControllerView(mRoot);
127 }
128
129 /* TODO REMOVE
130 public MediaControlView(Context context, boolean useFastForward) {
131 super(context);
132 mContext = context;
133 mUseFastForward = useFastForward;
134 initFloatingWindowLayout();
135 //initFloatingWindow();
136 }
137 */
138
139 /* TODO REMOVE
140 public MediaControlView(Context context) {
141 this(context, true);
142 }
143 */
144
145 /* T
146 private void initFloatingWindow() {
147 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
148 mWindow = PolicyManager.makeNewWindow(mContext);
149 mWindow.setWindowManager(mWindowManager, null, null);
150 mWindow.requestFeature(Window.FEATURE_NO_TITLE);
151 mDecor = mWindow.getDecorView();
152 mDecor.setOnTouchListener(mTouchListener);
153 mWindow.setContentView(this);
154 mWindow.setBackgroundDrawableResource(android.R.color.transparent);
155
156 // While the media controller is up, the volume control keys should
157 // affect the media stream type
158 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
159
160 setFocusable(true);
161 setFocusableInTouchMode(true);
162 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
163 requestFocus();
164 }
165 */
166
167 /*
168 // Allocate and initialize the static parts of mDecorLayoutParams. Must
169 // also call updateFloatingWindowLayout() to fill in the dynamic parts
170 // (y and width) before mDecorLayoutParams can be used.
171 private void initFloatingWindowLayout() {
172 mDecorLayoutParams = new WindowManager.LayoutParams();
173 WindowManager.LayoutParams p = mDecorLayoutParams;
174 p.gravity = Gravity.TOP;
175 p.height = LayoutParams.WRAP_CONTENT;
176 p.x = 0;
177 p.format = PixelFormat.TRANSLUCENT;
178 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
179 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
180 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
181 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
182 p.token = null;
183 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
184 }
185 */
186
187 // Update the dynamic parts of mDecorLayoutParams
188 // Must be called with mAnchor != NULL.
189 private void updateFloatingWindowLayout() {
190 int [] anchorPos = new int[2];
191 mAnchor.getLocationOnScreen(anchorPos);
192
193 WindowManager.LayoutParams p = mDecorLayoutParams;
194 p.width = mAnchor.getWidth();
195 p.y = anchorPos[1] + mAnchor.getHeight();
196 }
197
198 /*
199 // This is called whenever mAnchor's layout bound changes
200 public void onLayoutChange(View v, int left, int top, int right,
201 int bottom, int oldLeft, int oldTop, int oldRight,
202 int oldBottom) {
203 //updateFloatingWindowLayout();
204 if (mShowing) {
205 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
206 }
207 }
208 */
209
210 public boolean onTouch(View v, MotionEvent event) {
211 if (event.getAction() == MotionEvent.ACTION_DOWN) {
212 if (mShowing) {
213 hide();
214 }
215 }
216 return false;
217 }
218
219
220 public void setMediaPlayer(MediaPlayerControl player) {
221 mPlayer = player;
222 updatePausePlay();
223 }
224
225 /*
226 /**
227 * Set the view that acts as the anchor for the control view.
228 * This can for example be a VideoView, or your Activity's main view.
229 * @param view The view to which to anchor the controller when it is visible.
230 *-/
231 public void setAnchorView(View view) {
232 if (mAnchor != null) {
233 mAnchor.removeOnLayoutChangeListener(this);
234 }
235 mAnchor = view;
236 if (mAnchor != null) {
237 mAnchor.addOnLayoutChangeListener(this);
238 }
239
240 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
241 ViewGroup.LayoutParams.MATCH_PARENT,
242 ViewGroup.LayoutParams.MATCH_PARENT
243 );
244
245 removeAllViews();
246 View v = makeControllerView();
247 addView(v, frameParams);
248 }
249 */
250
251 /*
252 /**
253 * Create the view that holds the widgets that control playback.
254 * Derived classes can override this to create their own.
255 * @return The controller view.
256 * @hide This doesn't work as advertised
257 *-/
258 protected View makeControllerView() {
259 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
260 mRoot = inflate.inflate(R.layout.media_control, null);
261
262 initControllerView(mRoot);
263
264 return mRoot;
265 }
266 */
267
268 private void initControllerView(View v) {
269 mPauseButton = (ImageButton) v.findViewById(R.id.playBtn);
270 if (mPauseButton != null) {
271 mPauseButton.requestFocus();
272 mPauseButton.setOnClickListener(mPauseListener);
273 }
274
275 mFfwdButton = (ImageButton) v.findViewById(R.id.forwardBtn);
276 if (mFfwdButton != null) {
277 mFfwdButton.setOnClickListener(mFfwdListener);
278 if (!mFromXml) {
279 mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
280 }
281 }
282
283 mRewButton = (ImageButton) v.findViewById(R.id.rewindBtn);
284 if (mRewButton != null) {
285 mRewButton.setOnClickListener(mRewListener);
286 if (!mFromXml) {
287 mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
288 }
289 }
290
291 // By default these are hidden. They will be enabled when setPrevNextListeners() is called
292 mNextButton = (ImageButton) v.findViewById(R.id.nextBtn);
293 if (mNextButton != null && !mFromXml && !mListenersSet) {
294 mNextButton.setVisibility(View.GONE);
295 }
296 mPrevButton = (ImageButton) v.findViewById(R.id.previousBtn);
297 if (mPrevButton != null && !mFromXml && !mListenersSet) {
298 mPrevButton.setVisibility(View.GONE);
299 }
300
301 mProgress = (ProgressBar) v.findViewById(R.id.progressBar);
302 if (mProgress != null) {
303 if (mProgress instanceof SeekBar) {
304 SeekBar seeker = (SeekBar) mProgress;
305 seeker.setOnSeekBarChangeListener(mSeekListener);
306 }
307 mProgress.setMax(1000);
308 }
309
310 mEndTime = (TextView) v.findViewById(R.id.totalTimeText);
311 mCurrentTime = (TextView) v.findViewById(R.id.currentTimeText);
312 mFormatBuilder = new StringBuilder();
313 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
314
315 installPrevNextListeners();
316 }
317
318 /**
319 * Show the controller on screen. It will go away
320 * automatically after 3 seconds of inactivity.
321 */
322 public void show() {
323 show(sDefaultTimeout);
324 }
325
326 /**
327 * Disable pause or seek buttons if the stream cannot be paused or seeked.
328 * This requires the control interface to be a MediaPlayerControlExt
329 */
330 private void disableUnsupportedButtons() {
331 try {
332 if (mPauseButton != null && !mPlayer.canPause()) {
333 mPauseButton.setEnabled(false);
334 }
335 if (mRewButton != null && !mPlayer.canSeekBackward()) {
336 mRewButton.setEnabled(false);
337 }
338 if (mFfwdButton != null && !mPlayer.canSeekForward()) {
339 mFfwdButton.setEnabled(false);
340 }
341 } catch (IncompatibleClassChangeError ex) {
342 // We were given an old version of the interface, that doesn't have
343 // the canPause/canSeekXYZ methods. This is OK, it just means we
344 // assume the media can be paused and seeked, and so we don't disable
345 // the buttons.
346 }
347 }
348
349 /**
350 * Show the controller on screen. It will go away
351 * automatically after 'timeout' milliseconds of inactivity.
352 * @param timeout The timeout in milliseconds. Use 0 to show
353 * the controller until hide() is called.
354 */
355 public void show(int timeout) {
356 if (!mShowing && mAnchor != null) {
357 setProgress();
358 if (mPauseButton != null) {
359 mPauseButton.requestFocus();
360 }
361 disableUnsupportedButtons();
362 //updateFloatingWindowLayout();
363 mWindowManager.addView(mDecor, mDecorLayoutParams);
364 mShowing = true;
365 }
366 updatePausePlay();
367
368 // cause the progress bar to be updated even if mShowing
369 // was already true. This happens, for example, if we're
370 // paused with the progress bar showing the user hits play.
371 mHandler.sendEmptyMessage(SHOW_PROGRESS);
372
373 Message msg = mHandler.obtainMessage(FADE_OUT);
374 if (timeout != 0) {
375 mHandler.removeMessages(FADE_OUT);
376 mHandler.sendMessageDelayed(msg, timeout);
377 }
378 }
379
380 public boolean isShowing() {
381 return mShowing;
382 }
383
384 /**
385 * Remove the controller from the screen.
386 */
387 public void hide() {
388 /*
389 if (mAnchor == null)
390 return;
391
392 if (mShowing) {
393 try {
394 mHandler.removeMessages(SHOW_PROGRESS);
395 mWindowManager.removeView(mDecor);
396 } catch (IllegalArgumentException ex) {
397 Log.w(TAG, "already removed");
398 }
399 mShowing = false;
400 }
401 */
402 }
403
404 private Handler mHandler = new Handler() {
405 @Override
406 public void handleMessage(Message msg) {
407 int pos;
408 switch (msg.what) {
409 case FADE_OUT:
410 hide();
411 break;
412 case SHOW_PROGRESS:
413 pos = setProgress();
414 if (!mDragging && mShowing && mPlayer.isPlaying()) {
415 msg = obtainMessage(SHOW_PROGRESS);
416 sendMessageDelayed(msg, 1000 - (pos % 1000));
417 }
418 break;
419 }
420 }
421 };
422
423 private String stringForTime(int timeMs) {
424 int totalSeconds = timeMs / 1000;
425
426 int seconds = totalSeconds % 60;
427 int minutes = (totalSeconds / 60) % 60;
428 int hours = totalSeconds / 3600;
429
430 mFormatBuilder.setLength(0);
431 if (hours > 0) {
432 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
433 } else {
434 return mFormatter.format("%02d:%02d", minutes, seconds).toString();
435 }
436 }
437
438 private int setProgress() {
439 if (mPlayer == null || mDragging) {
440 return 0;
441 }
442 int position = mPlayer.getCurrentPosition();
443 int duration = mPlayer.getDuration();
444 if (mProgress != null) {
445 if (duration > 0) {
446 // use long to avoid overflow
447 long pos = 1000L * position / duration;
448 mProgress.setProgress( (int) pos);
449 }
450 int percent = mPlayer.getBufferPercentage();
451 mProgress.setSecondaryProgress(percent * 10);
452 }
453
454 if (mEndTime != null)
455 mEndTime.setText(stringForTime(duration));
456 if (mCurrentTime != null)
457 mCurrentTime.setText(stringForTime(position));
458
459 return position;
460 }
461
462 @Override
463 public boolean onTouchEvent(MotionEvent event) {
464 show(sDefaultTimeout);
465 return true;
466 }
467
468 @Override
469 public boolean onTrackballEvent(MotionEvent ev) {
470 show(sDefaultTimeout);
471 return false;
472 }
473
474 @Override
475 public boolean dispatchKeyEvent(KeyEvent event) {
476 int keyCode = event.getKeyCode();
477 final boolean uniqueDown = event.getRepeatCount() == 0
478 && event.getAction() == KeyEvent.ACTION_DOWN;
479 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
480 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
481 || keyCode == KeyEvent.KEYCODE_SPACE) {
482 if (uniqueDown) {
483 doPauseResume();
484 show(sDefaultTimeout);
485 if (mPauseButton != null) {
486 mPauseButton.requestFocus();
487 }
488 }
489 return true;
490 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
491 if (uniqueDown && !mPlayer.isPlaying()) {
492 mPlayer.start();
493 updatePausePlay();
494 show(sDefaultTimeout);
495 }
496 return true;
497 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
498 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
499 if (uniqueDown && mPlayer.isPlaying()) {
500 mPlayer.pause();
501 updatePausePlay();
502 show(sDefaultTimeout);
503 }
504 return true;
505 } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
506 || keyCode == KeyEvent.KEYCODE_VOLUME_UP
507 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
508 || keyCode == KeyEvent.KEYCODE_CAMERA) {
509 // don't show the controls for volume adjustment
510 return super.dispatchKeyEvent(event);
511 } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
512 if (uniqueDown) {
513 hide();
514 }
515 return true;
516 }
517
518 show(sDefaultTimeout);
519 return super.dispatchKeyEvent(event);
520 }
521
522 private View.OnClickListener mPauseListener = new View.OnClickListener() {
523 public void onClick(View v) {
524 doPauseResume();
525 show(sDefaultTimeout);
526 }
527 };
528
529 private void updatePausePlay() {
530 if (mRoot == null || mPauseButton == null)
531 return;
532
533 if (mPlayer.isPlaying()) {
534 mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
535 } else {
536 mPauseButton.setImageResource(android.R.drawable.ic_media_play);
537 }
538 }
539
540 private void doPauseResume() {
541 if (mPlayer.isPlaying()) {
542 mPlayer.pause();
543 } else {
544 mPlayer.start();
545 }
546 updatePausePlay();
547 }
548
549 // There are two scenarios that can trigger the seekbar listener to trigger:
550 //
551 // The first is the user using the touchpad to adjust the posititon of the
552 // seekbar's thumb. In this case onStartTrackingTouch is called followed by
553 // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
554 // We're setting the field "mDragging" to true for the duration of the dragging
555 // session to avoid jumps in the position in case of ongoing playback.
556 //
557 // The second scenario involves the user operating the scroll ball, in this
558 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
559 // we will simply apply the updated position without suspending regular updates.
560 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
561 public void onStartTrackingTouch(SeekBar bar) {
562 show(3600000);
563
564 mDragging = true;
565
566 // By removing these pending progress messages we make sure
567 // that a) we won't update the progress while the user adjusts
568 // the seekbar and b) once the user is done dragging the thumb
569 // we will post one of these messages to the queue again and
570 // this ensures that there will be exactly one message queued up.
571 mHandler.removeMessages(SHOW_PROGRESS);
572 }
573
574 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
575 if (!fromuser) {
576 // We're not interested in programmatically generated changes to
577 // the progress bar's position.
578 return;
579 }
580
581 long duration = mPlayer.getDuration();
582 long newposition = (duration * progress) / 1000L;
583 mPlayer.seekTo( (int) newposition);
584 if (mCurrentTime != null)
585 mCurrentTime.setText(stringForTime( (int) newposition));
586 }
587
588 public void onStopTrackingTouch(SeekBar bar) {
589 mDragging = false;
590 setProgress();
591 updatePausePlay();
592 show(sDefaultTimeout);
593
594 // Ensure that progress is properly updated in the future,
595 // the call to show() does not guarantee this because it is a
596 // no-op if we are already showing.
597 mHandler.sendEmptyMessage(SHOW_PROGRESS);
598 }
599 };
600
601 @Override
602 public void setEnabled(boolean enabled) {
603 if (mPauseButton != null) {
604 mPauseButton.setEnabled(enabled);
605 }
606 if (mFfwdButton != null) {
607 mFfwdButton.setEnabled(enabled);
608 }
609 if (mRewButton != null) {
610 mRewButton.setEnabled(enabled);
611 }
612 if (mNextButton != null) {
613 mNextButton.setEnabled(enabled && mNextListener != null);
614 }
615 if (mPrevButton != null) {
616 mPrevButton.setEnabled(enabled && mPrevListener != null);
617 }
618 if (mProgress != null) {
619 mProgress.setEnabled(enabled);
620 }
621 disableUnsupportedButtons();
622 super.setEnabled(enabled);
623 }
624
625 @Override
626 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
627 super.onInitializeAccessibilityEvent(event);
628 event.setClassName(MediaControlView.class.getName());
629 }
630
631 @Override
632 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
633 super.onInitializeAccessibilityNodeInfo(info);
634 info.setClassName(MediaControlView.class.getName());
635 }
636
637 private View.OnClickListener mRewListener = new View.OnClickListener() {
638 public void onClick(View v) {
639 int pos = mPlayer.getCurrentPosition();
640 pos -= 5000; // milliseconds
641 mPlayer.seekTo(pos);
642 setProgress();
643
644 show(sDefaultTimeout);
645 }
646 };
647
648 private View.OnClickListener mFfwdListener = new View.OnClickListener() {
649 public void onClick(View v) {
650 int pos = mPlayer.getCurrentPosition();
651 pos += 15000; // milliseconds
652 mPlayer.seekTo(pos);
653 setProgress();
654
655 show(sDefaultTimeout);
656 }
657 };
658
659 private void installPrevNextListeners() {
660 if (mNextButton != null) {
661 mNextButton.setOnClickListener(mNextListener);
662 mNextButton.setEnabled(mNextListener != null);
663 }
664
665 if (mPrevButton != null) {
666 mPrevButton.setOnClickListener(mPrevListener);
667 mPrevButton.setEnabled(mPrevListener != null);
668 }
669 }
670
671 public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
672 mNextListener = next;
673 mPrevListener = prev;
674 mListenersSet = true;
675
676 if (mRoot != null) {
677 installPrevNextListeners();
678
679 if (mNextButton != null && !mFromXml) {
680 mNextButton.setVisibility(View.VISIBLE);
681 }
682 if (mPrevButton != null && !mFromXml) {
683 mPrevButton.setVisibility(View.VISIBLE);
684 }
685 }
686 }
687
688 }