1 /* ownCloud Android client application
3 * Copyright (C) 2012-2013 ownCloud Inc.
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.
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.
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/>.
19 package com
.owncloud
.android
.media
;
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
;
49 import java
.util
.Formatter
;
50 import java
.util
.Locale
;
52 import com
.owncloud
.android
.R
;
55 * View containing controls for a {@link MediaPlayer}.
57 * Holds buttons "play / pause", "rewind", "fast forward"
58 * and a progress slider.
60 * It synchronizes itself with the state of the
61 * {@link MediaPlayer}.
63 * @author David A. Velasco
66 public class MediaControlView
extends FrameLayout
implements /* OnLayoutChangeListener, */ OnTouchListener
{
68 private static final String TAG
= MediaControlView
.class.getSimpleName();
71 private MediaPlayerControl mPlayer
;
72 private Context mContext
;
75 private WindowManager mWindowManager
;
76 //private Window mWindow;
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
;
92 private ImageButton mPauseButton
;
93 private ImageButton mFfwdButton
;
94 private ImageButton mRewButton
;
95 private ImageButton mNextButton
;
96 private ImageButton mPrevButton
;
98 public MediaControlView(Context context
, AttributeSet attrs
) {
99 super(context
, attrs
);
100 //mRoot = this; // TODO review if this is adequate
102 mUseFastForward
= true
;
105 mWindowManager
= (WindowManager
)mContext
.getSystemService(Context
.WINDOW_SERVICE
);
107 FrameLayout
.LayoutParams frameParams
= new FrameLayout
.LayoutParams(
108 ViewGroup
.LayoutParams
.MATCH_PARENT
,
109 ViewGroup
.LayoutParams
.MATCH_PARENT
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
);
118 setFocusableInTouchMode(true
);
119 setDescendantFocusability(ViewGroup
.FOCUS_AFTER_DESCENDANTS
);
124 public void onFinishInflate() {
126 initControllerView(mRoot
);
130 public MediaControlView(Context context, boolean useFastForward) {
133 mUseFastForward = useFastForward;
134 initFloatingWindowLayout();
135 //initFloatingWindow();
140 public MediaControlView(Context context) {
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);
156 // While the media controller is up, the volume control keys should
157 // affect the media stream type
158 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
161 setFocusableInTouchMode(true);
162 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
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;
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;
183 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
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
);
193 WindowManager
.LayoutParams p
= mDecorLayoutParams
;
194 p
.width
= mAnchor
.getWidth();
195 p
.y
= anchorPos
[1] + mAnchor
.getHeight();
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,
203 //updateFloatingWindowLayout();
205 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
210 public boolean onTouch(View v
, MotionEvent event
) {
211 if (event
.getAction() == MotionEvent
.ACTION_DOWN
) {
220 public void setMediaPlayer(MediaPlayerControl player
) {
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.
231 public void setAnchorView(View view) {
232 if (mAnchor != null) {
233 mAnchor.removeOnLayoutChangeListener(this);
236 if (mAnchor != null) {
237 mAnchor.addOnLayoutChangeListener(this);
240 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
241 ViewGroup.LayoutParams.MATCH_PARENT,
242 ViewGroup.LayoutParams.MATCH_PARENT
246 View v = makeControllerView();
247 addView(v, frameParams);
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
258 protected View makeControllerView() {
259 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
260 mRoot = inflate.inflate(R.layout.media_control, null);
262 initControllerView(mRoot);
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
);
275 mFfwdButton
= (ImageButton
) v
.findViewById(R
.id
.forwardBtn
);
276 if (mFfwdButton
!= null
) {
277 mFfwdButton
.setOnClickListener(mFfwdListener
);
279 mFfwdButton
.setVisibility(mUseFastForward ? View
.VISIBLE
: View
.GONE
);
283 mRewButton
= (ImageButton
) v
.findViewById(R
.id
.rewindBtn
);
284 if (mRewButton
!= null
) {
285 mRewButton
.setOnClickListener(mRewListener
);
287 mRewButton
.setVisibility(mUseFastForward ? View
.VISIBLE
: View
.GONE
);
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
);
296 mPrevButton
= (ImageButton
) v
.findViewById(R
.id
.previousBtn
);
297 if (mPrevButton
!= null
&& !mFromXml
&& !mListenersSet
) {
298 mPrevButton
.setVisibility(View
.GONE
);
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
);
307 mProgress
.setMax(1000);
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());
315 installPrevNextListeners();
319 * Show the controller on screen. It will go away
320 * automatically after 3 seconds of inactivity.
323 show(sDefaultTimeout
);
327 * Disable pause or seek buttons if the stream cannot be paused or seeked.
328 * This requires the control interface to be a MediaPlayerControlExt
330 private void disableUnsupportedButtons() {
332 if (mPauseButton
!= null
&& !mPlayer
.canPause()) {
333 mPauseButton
.setEnabled(false
);
335 if (mRewButton
!= null
&& !mPlayer
.canSeekBackward()) {
336 mRewButton
.setEnabled(false
);
338 if (mFfwdButton
!= null
&& !mPlayer
.canSeekForward()) {
339 mFfwdButton
.setEnabled(false
);
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
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.
355 public void show(int timeout
) {
356 if (!mShowing
&& mAnchor
!= null
) {
358 if (mPauseButton
!= null
) {
359 mPauseButton
.requestFocus();
361 disableUnsupportedButtons();
362 //updateFloatingWindowLayout();
363 mWindowManager
.addView(mDecor
, mDecorLayoutParams
);
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
);
373 Message msg
= mHandler
.obtainMessage(FADE_OUT
);
375 mHandler
.removeMessages(FADE_OUT
);
376 mHandler
.sendMessageDelayed(msg
, timeout
);
380 public boolean isShowing() {
385 * Remove the controller from the screen.
394 mHandler.removeMessages(SHOW_PROGRESS);
395 mWindowManager.removeView(mDecor);
396 } catch (IllegalArgumentException ex) {
397 Log.w(TAG, "already removed");
404 private Handler mHandler
= new Handler() {
406 public void handleMessage(Message msg
) {
414 if (!mDragging
&& mShowing
&& mPlayer
.isPlaying()) {
415 msg
= obtainMessage(SHOW_PROGRESS
);
416 sendMessageDelayed(msg
, 1000 - (pos
% 1000));
423 private String
stringForTime(int timeMs
) {
424 int totalSeconds
= timeMs
/ 1000;
426 int seconds
= totalSeconds
% 60;
427 int minutes
= (totalSeconds
/ 60) % 60;
428 int hours
= totalSeconds
/ 3600;
430 mFormatBuilder
.setLength(0);
432 return mFormatter
.format("%d:%02d:%02d", hours
, minutes
, seconds
).toString();
434 return mFormatter
.format("%02d:%02d", minutes
, seconds
).toString();
438 private int setProgress() {
439 if (mPlayer
== null
|| mDragging
) {
442 int position
= mPlayer
.getCurrentPosition();
443 int duration
= mPlayer
.getDuration();
444 if (mProgress
!= null
) {
446 // use long to avoid overflow
447 long pos
= 1000L * position
/ duration
;
448 mProgress
.setProgress( (int) pos
);
450 int percent
= mPlayer
.getBufferPercentage();
451 mProgress
.setSecondaryProgress(percent
* 10);
454 if (mEndTime
!= null
)
455 mEndTime
.setText(stringForTime(duration
));
456 if (mCurrentTime
!= null
)
457 mCurrentTime
.setText(stringForTime(position
));
463 public boolean onTouchEvent(MotionEvent event
) {
464 show(sDefaultTimeout
);
469 public boolean onTrackballEvent(MotionEvent ev
) {
470 show(sDefaultTimeout
);
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
) {
484 show(sDefaultTimeout
);
485 if (mPauseButton
!= null
) {
486 mPauseButton
.requestFocus();
490 } else if (keyCode
== KeyEvent
.KEYCODE_MEDIA_PLAY
) {
491 if (uniqueDown
&& !mPlayer
.isPlaying()) {
494 show(sDefaultTimeout
);
497 } else if (keyCode
== KeyEvent
.KEYCODE_MEDIA_STOP
498 || keyCode
== KeyEvent
.KEYCODE_MEDIA_PAUSE
) {
499 if (uniqueDown
&& mPlayer
.isPlaying()) {
502 show(sDefaultTimeout
);
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
) {
518 show(sDefaultTimeout
);
519 return super.dispatchKeyEvent(event
);
522 private View
.OnClickListener mPauseListener
= new View
.OnClickListener() {
523 public void onClick(View v
) {
525 show(sDefaultTimeout
);
529 private void updatePausePlay() {
530 if (mRoot
== null
|| mPauseButton
== null
)
533 if (mPlayer
.isPlaying()) {
534 mPauseButton
.setImageResource(android
.R
.drawable
.ic_media_pause
);
536 mPauseButton
.setImageResource(android
.R
.drawable
.ic_media_play
);
540 private void doPauseResume() {
541 if (mPlayer
.isPlaying()) {
549 // There are two scenarios that can trigger the seekbar listener to trigger:
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.
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
) {
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
);
574 public void onProgressChanged(SeekBar bar
, int progress
, boolean fromuser
) {
576 // We're not interested in programmatically generated changes to
577 // the progress bar's position.
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
));
588 public void onStopTrackingTouch(SeekBar bar
) {
592 show(sDefaultTimeout
);
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
);
602 public void setEnabled(boolean enabled
) {
603 if (mPauseButton
!= null
) {
604 mPauseButton
.setEnabled(enabled
);
606 if (mFfwdButton
!= null
) {
607 mFfwdButton
.setEnabled(enabled
);
609 if (mRewButton
!= null
) {
610 mRewButton
.setEnabled(enabled
);
612 if (mNextButton
!= null
) {
613 mNextButton
.setEnabled(enabled
&& mNextListener
!= null
);
615 if (mPrevButton
!= null
) {
616 mPrevButton
.setEnabled(enabled
&& mPrevListener
!= null
);
618 if (mProgress
!= null
) {
619 mProgress
.setEnabled(enabled
);
621 disableUnsupportedButtons();
622 super.setEnabled(enabled
);
626 public void onInitializeAccessibilityEvent(AccessibilityEvent event
) {
627 super.onInitializeAccessibilityEvent(event
);
628 event
.setClassName(MediaControlView
.class.getName());
632 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info
) {
633 super.onInitializeAccessibilityNodeInfo(info
);
634 info
.setClassName(MediaControlView
.class.getName());
637 private View
.OnClickListener mRewListener
= new View
.OnClickListener() {
638 public void onClick(View v
) {
639 int pos
= mPlayer
.getCurrentPosition();
640 pos
-= 5000; // milliseconds
644 show(sDefaultTimeout
);
648 private View
.OnClickListener mFfwdListener
= new View
.OnClickListener() {
649 public void onClick(View v
) {
650 int pos
= mPlayer
.getCurrentPosition();
651 pos
+= 15000; // milliseconds
655 show(sDefaultTimeout
);
659 private void installPrevNextListeners() {
660 if (mNextButton
!= null
) {
661 mNextButton
.setOnClickListener(mNextListener
);
662 mNextButton
.setEnabled(mNextListener
!= null
);
665 if (mPrevButton
!= null
) {
666 mPrevButton
.setOnClickListener(mPrevListener
);
667 mPrevButton
.setEnabled(mPrevListener
!= null
);
671 public void setPrevNextListeners(View
.OnClickListener next
, View
.OnClickListener prev
) {
672 mNextListener
= next
;
673 mPrevListener
= prev
;
674 mListenersSet
= true
;
677 installPrevNextListeners();
679 if (mNextButton
!= null
&& !mFromXml
) {
680 mNextButton
.setVisibility(View
.VISIBLE
);
682 if (mPrevButton
!= null
&& !mFromXml
) {
683 mPrevButton
.setVisibility(View
.VISIBLE
);