Improving our own media control: removed 'next' and 'previous' buttons, removed undes...
[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.media.MediaPlayer;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.View.OnClickListener;
30 import android.view.ViewGroup;
31 import android.view.accessibility.AccessibilityEvent;
32 import android.view.accessibility.AccessibilityNodeInfo;
33 import android.widget.FrameLayout;
34 import android.widget.ImageButton;
35 import android.widget.MediaController.MediaPlayerControl;
36 import android.widget.ProgressBar;
37 import android.widget.SeekBar;
38 import android.widget.SeekBar.OnSeekBarChangeListener;
39 import android.widget.TextView;
40
41 import java.util.Formatter;
42 import java.util.Locale;
43
44 import com.owncloud.android.R;
45
46 /**
47 * View containing controls for a {@link MediaPlayer}.
48 *
49 * Holds buttons "play / pause", "rewind", "fast forward"
50 * and a progress slider.
51 *
52 * It synchronizes itself with the state of the
53 * {@link MediaPlayer}.
54 *
55 * @author David A. Velasco
56 */
57
58 public class MediaControlView extends FrameLayout /* implements OnLayoutChangeListener, OnTouchListener */ implements OnClickListener, OnSeekBarChangeListener {
59
60 private MediaPlayerControl mPlayer;
61 private Context mContext;
62 private View mRoot;
63 private ProgressBar mProgress;
64 private TextView mEndTime, mCurrentTime;
65 private boolean mDragging;
66 private static final int SHOW_PROGRESS = 1;
67 StringBuilder mFormatBuilder;
68 Formatter mFormatter;
69 private ImageButton mPauseButton;
70 private ImageButton mFfwdButton;
71 private ImageButton mRewButton;
72
73 public MediaControlView(Context context, AttributeSet attrs) {
74 super(context, attrs);
75 mContext = context;
76
77 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
78 ViewGroup.LayoutParams.MATCH_PARENT,
79 ViewGroup.LayoutParams.MATCH_PARENT
80 );
81 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
82 mRoot = inflate.inflate(R.layout.media_control, null);
83 initControllerView(mRoot);
84 addView(mRoot, frameParams);
85
86 setFocusable(true);
87 setFocusableInTouchMode(true);
88 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
89 requestFocus();
90 }
91
92 @Override
93 public void onFinishInflate() {
94 /*
95 if (mRoot != null)
96 initControllerView(mRoot);
97 */
98 }
99
100 /* TODO REMOVE
101 public MediaControlView(Context context, boolean useFastForward) {
102 super(context);
103 mContext = context;
104 mUseFastForward = useFastForward;
105 initFloatingWindowLayout();
106 //initFloatingWindow();
107 }
108 */
109
110 /* TODO REMOVE
111 public MediaControlView(Context context) {
112 this(context, true);
113 }
114 */
115
116 /* T
117 private void initFloatingWindow() {
118 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
119 mWindow = PolicyManager.makeNewWindow(mContext);
120 mWindow.setWindowManager(mWindowManager, null, null);
121 mWindow.requestFeature(Window.FEATURE_NO_TITLE);
122 mDecor = mWindow.getDecorView();
123 mDecor.setOnTouchListener(mTouchListener);
124 mWindow.setContentView(this);
125 mWindow.setBackgroundDrawableResource(android.R.color.transparent);
126
127 // While the media controller is up, the volume control keys should
128 // affect the media stream type
129 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
130
131 setFocusable(true);
132 setFocusableInTouchMode(true);
133 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
134 requestFocus();
135 }
136 */
137
138 /*
139 // Allocate and initialize the static parts of mDecorLayoutParams. Must
140 // also call updateFloatingWindowLayout() to fill in the dynamic parts
141 // (y and width) before mDecorLayoutParams can be used.
142 private void initFloatingWindowLayout() {
143 mDecorLayoutParams = new WindowManager.LayoutParams();
144 WindowManager.LayoutParams p = mDecorLayoutParams;
145 p.gravity = Gravity.TOP;
146 p.height = LayoutParams.WRAP_CONTENT;
147 p.x = 0;
148 p.format = PixelFormat.TRANSLUCENT;
149 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
150 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
151 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
152 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
153 p.token = null;
154 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
155 }
156 */
157
158 // Update the dynamic parts of mDecorLayoutParams
159 // Must be called with mAnchor != NULL.
160 /*
161 private void updateFloatingWindowLayout() {
162 int [] anchorPos = new int[2];
163 mAnchor.getLocationOnScreen(anchorPos);
164
165 WindowManager.LayoutParams p = mDecorLayoutParams;
166 p.width = mAnchor.getWidth();
167 p.y = anchorPos[1] + mAnchor.getHeight();
168 }
169 */
170
171 /*
172 // This is called whenever mAnchor's layout bound changes
173 public void onLayoutChange(View v, int left, int top, int right,
174 int bottom, int oldLeft, int oldTop, int oldRight,
175 int oldBottom) {
176 //updateFloatingWindowLayout();
177 if (mShowing) {
178 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
179 }
180 }
181 */
182
183 /*
184 public boolean onTouch(View v, MotionEvent event) {
185 if (event.getAction() == MotionEvent.ACTION_DOWN) {
186 if (mShowing) {
187 hide();
188 }
189 }
190 return false;
191 }
192 */
193
194
195 public void setMediaPlayer(MediaPlayerControl player) {
196 mPlayer = player;
197 mHandler.sendEmptyMessage(SHOW_PROGRESS);
198 updatePausePlay();
199 }
200
201
202 private void initControllerView(View v) {
203 mPauseButton = (ImageButton) v.findViewById(R.id.playBtn);
204 if (mPauseButton != null) {
205 mPauseButton.requestFocus();
206 mPauseButton.setOnClickListener(this);
207 }
208
209 mFfwdButton = (ImageButton) v.findViewById(R.id.forwardBtn);
210 if (mFfwdButton != null) {
211 mFfwdButton.setOnClickListener(this);
212 }
213
214 mRewButton = (ImageButton) v.findViewById(R.id.rewindBtn);
215 if (mRewButton != null) {
216 mRewButton.setOnClickListener(this);
217 }
218
219 mProgress = (ProgressBar) v.findViewById(R.id.progressBar);
220 if (mProgress != null) {
221 if (mProgress instanceof SeekBar) {
222 SeekBar seeker = (SeekBar) mProgress;
223 seeker.setOnSeekBarChangeListener(this);
224 }
225 mProgress.setMax(1000);
226 }
227
228 mEndTime = (TextView) v.findViewById(R.id.totalTimeText);
229 mCurrentTime = (TextView) v.findViewById(R.id.currentTimeText);
230 mFormatBuilder = new StringBuilder();
231 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
232
233 }
234
235
236 /**
237 * Disable pause or seek buttons if the stream cannot be paused or seeked.
238 * This requires the control interface to be a MediaPlayerControlExt
239 */
240 private void disableUnsupportedButtons() {
241 try {
242 if (mPauseButton != null && !mPlayer.canPause()) {
243 mPauseButton.setEnabled(false);
244 }
245 if (mRewButton != null && !mPlayer.canSeekBackward()) {
246 mRewButton.setEnabled(false);
247 }
248 if (mFfwdButton != null && !mPlayer.canSeekForward()) {
249 mFfwdButton.setEnabled(false);
250 }
251 } catch (IncompatibleClassChangeError ex) {
252 // We were given an old version of the interface, that doesn't have
253 // the canPause/canSeekXYZ methods. This is OK, it just means we
254 // assume the media can be paused and seeked, and so we don't disable
255 // the buttons.
256 }
257 }
258
259
260 private Handler mHandler = new Handler() {
261 @Override
262 public void handleMessage(Message msg) {
263 int pos;
264 switch (msg.what) {
265 case SHOW_PROGRESS:
266 pos = setProgress();
267 if (!mDragging) {
268 msg = obtainMessage(SHOW_PROGRESS);
269 sendMessageDelayed(msg, 1000 - (pos % 1000));
270 }
271 break;
272 }
273 }
274 };
275
276 private String stringForTime(int timeMs) {
277 int totalSeconds = timeMs / 1000;
278
279 int seconds = totalSeconds % 60;
280 int minutes = (totalSeconds / 60) % 60;
281 int hours = totalSeconds / 3600;
282
283 mFormatBuilder.setLength(0);
284 if (hours > 0) {
285 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
286 } else {
287 return mFormatter.format("%02d:%02d", minutes, seconds).toString();
288 }
289 }
290
291 private int setProgress() {
292 if (mPlayer == null || mDragging) {
293 return 0;
294 }
295 int position = mPlayer.getCurrentPosition();
296 int duration = mPlayer.getDuration();
297 if (mProgress != null) {
298 if (duration > 0) {
299 // use long to avoid overflow
300 long pos = 1000L * position / duration;
301 mProgress.setProgress( (int) pos);
302 }
303 int percent = mPlayer.getBufferPercentage();
304 mProgress.setSecondaryProgress(percent * 10);
305 }
306
307 if (mEndTime != null)
308 mEndTime.setText(stringForTime(duration));
309 if (mCurrentTime != null)
310 mCurrentTime.setText(stringForTime(position));
311
312 return position;
313 }
314
315
316 @Override
317 public boolean dispatchKeyEvent(KeyEvent event) {
318 int keyCode = event.getKeyCode();
319 final boolean uniqueDown = event.getRepeatCount() == 0
320 && event.getAction() == KeyEvent.ACTION_DOWN;
321 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
322 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
323 || keyCode == KeyEvent.KEYCODE_SPACE) {
324 if (uniqueDown) {
325 doPauseResume();
326 //show(sDefaultTimeout);
327 if (mPauseButton != null) {
328 mPauseButton.requestFocus();
329 }
330 }
331 return true;
332 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
333 if (uniqueDown && !mPlayer.isPlaying()) {
334 mPlayer.start();
335 updatePausePlay();
336 //show(sDefaultTimeout);
337 }
338 return true;
339 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
340 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
341 if (uniqueDown && mPlayer.isPlaying()) {
342 mPlayer.pause();
343 updatePausePlay();
344 //show(sDefaultTimeout);
345 }
346 return true;
347 }
348
349 //show(sDefaultTimeout);
350 return super.dispatchKeyEvent(event);
351 }
352
353 public void updatePausePlay() {
354 if (mRoot == null || mPauseButton == null)
355 return;
356
357 if (mPlayer.isPlaying()) {
358 mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
359 } else {
360 mPauseButton.setImageResource(android.R.drawable.ic_media_play);
361 }
362 }
363
364 private void doPauseResume() {
365 if (mPlayer.isPlaying()) {
366 mPlayer.pause();
367 } else {
368 mPlayer.start();
369 }
370 updatePausePlay();
371 }
372
373 @Override
374 public void setEnabled(boolean enabled) {
375 if (mPauseButton != null) {
376 mPauseButton.setEnabled(enabled);
377 }
378 if (mFfwdButton != null) {
379 mFfwdButton.setEnabled(enabled);
380 }
381 if (mRewButton != null) {
382 mRewButton.setEnabled(enabled);
383 }
384 if (mProgress != null) {
385 mProgress.setEnabled(enabled);
386 }
387 disableUnsupportedButtons();
388 super.setEnabled(enabled);
389 }
390
391 @Override
392 public void onClick(View v) {
393 int pos;
394 switch (v.getId()) {
395
396 case R.id.playBtn:
397 doPauseResume();
398 break;
399
400 case R.id.rewindBtn:
401 pos = mPlayer.getCurrentPosition();
402 pos -= 5000;
403 mPlayer.seekTo(pos);
404 setProgress();
405 break;
406
407 case R.id.forwardBtn:
408 pos = mPlayer.getCurrentPosition();
409 pos += 15000;
410 mPlayer.seekTo(pos);
411 setProgress();
412 break;
413
414 }
415 }
416
417
418 @Override
419 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
420 if (!fromUser) {
421 // We're not interested in programmatically generated changes to
422 // the progress bar's position.
423 return;
424 }
425
426 long duration = mPlayer.getDuration();
427 long newposition = (duration * progress) / 1000L;
428 mPlayer.seekTo( (int) newposition);
429 if (mCurrentTime != null)
430 mCurrentTime.setText(stringForTime( (int) newposition));
431 }
432
433 /**
434 * Called in devices with touchpad when the user starts to adjust the
435 * position of the seekbar's thumb.
436 *
437 * Will be followed by several onProgressChanged notifications.
438 */
439 @Override
440 public void onStartTrackingTouch(SeekBar seekBar) {
441 mDragging = true; // monitors the duration of dragging
442 mHandler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging
443 }
444
445
446 /**
447 * Called in devices with touchpad when the user finishes the
448 * adjusting of the seekbar.
449 */
450 @Override
451 public void onStopTrackingTouch(SeekBar seekBar) {
452 mDragging = false;
453 setProgress();
454 updatePausePlay();
455 mHandler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress
456 }
457
458 @Override
459 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
460 super.onInitializeAccessibilityEvent(event);
461 event.setClassName(MediaControlView.class.getName());
462 }
463
464 @Override
465 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
466 super.onInitializeAccessibilityNodeInfo(info);
467 info.setClassName(MediaControlView.class.getName());
468 }
469
470 }