MediaControlView.java 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /*
  2. * ownCloud Android client application
  3. *
  4. * @author David A. Velasco
  5. * Copyright (C) 2015 ownCloud Inc.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. package com.owncloud.android.media;
  20. import android.content.Context;
  21. import android.media.MediaPlayer;
  22. import android.os.Handler;
  23. import android.os.Message;
  24. import android.util.AttributeSet;
  25. import android.view.KeyEvent;
  26. import android.view.LayoutInflater;
  27. import android.view.View;
  28. import android.view.View.OnClickListener;
  29. import android.view.ViewGroup;
  30. import android.view.accessibility.AccessibilityEvent;
  31. import android.view.accessibility.AccessibilityNodeInfo;
  32. import android.widget.FrameLayout;
  33. import android.widget.ImageButton;
  34. import android.widget.MediaController.MediaPlayerControl;
  35. import android.widget.ProgressBar;
  36. import android.widget.SeekBar;
  37. import android.widget.SeekBar.OnSeekBarChangeListener;
  38. import android.widget.TextView;
  39. import com.owncloud.android.R;
  40. import com.owncloud.android.lib.common.utils.Log_OC;
  41. import com.owncloud.android.utils.ThemeUtils;
  42. import java.util.Formatter;
  43. import java.util.Locale;
  44. /**
  45. * View containing controls for a {@link MediaPlayer}.
  46. * <p>
  47. * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider.
  48. * <p>
  49. * It synchronizes itself with the state of the {@link MediaPlayer}.
  50. */
  51. public class MediaControlView extends FrameLayout implements OnClickListener, OnSeekBarChangeListener {
  52. private static final String TAG = MediaControlView.class.getSimpleName();
  53. private static final int SHOW_PROGRESS = 1;
  54. private MediaPlayerControl playerControl;
  55. private View root;
  56. private ProgressBar progressBar;
  57. private TextView endTime;
  58. private TextView currentTime;
  59. private boolean isDragging;
  60. private ImageButton pauseButton;
  61. private ImageButton forwardButton;
  62. private ImageButton rewindButton;
  63. public MediaControlView(Context context, AttributeSet attrs) {
  64. super(context, attrs);
  65. FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
  66. ViewGroup.LayoutParams.MATCH_PARENT,
  67. ViewGroup.LayoutParams.MATCH_PARENT
  68. );
  69. LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  70. root = inflate.inflate(R.layout.media_control, null);
  71. initControllerView(root);
  72. addView(root, frameParams);
  73. setFocusable(true);
  74. setFocusableInTouchMode(true);
  75. setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
  76. requestFocus();
  77. }
  78. @Override
  79. public void onFinishInflate() {
  80. super.onFinishInflate();
  81. }
  82. public void setMediaPlayer(MediaPlayerControl player) {
  83. playerControl = player;
  84. handler.sendEmptyMessage(SHOW_PROGRESS);
  85. handler.postDelayed(()-> {
  86. updatePausePlay();
  87. setProgress();
  88. }, 100);
  89. }
  90. public void stopMediaPlayerMessages() {
  91. handler.removeMessages(SHOW_PROGRESS);
  92. }
  93. private void initControllerView(View v) {
  94. pauseButton = v.findViewById(R.id.playBtn);
  95. if (pauseButton != null) {
  96. pauseButton.requestFocus();
  97. pauseButton.setOnClickListener(this);
  98. }
  99. forwardButton = v.findViewById(R.id.forwardBtn);
  100. if (forwardButton != null) {
  101. forwardButton.setOnClickListener(this);
  102. }
  103. rewindButton = v.findViewById(R.id.rewindBtn);
  104. if (rewindButton != null) {
  105. rewindButton.setOnClickListener(this);
  106. }
  107. progressBar = v.findViewById(R.id.progressBar);
  108. if (progressBar != null) {
  109. if (progressBar instanceof SeekBar) {
  110. SeekBar seeker = (SeekBar) progressBar;
  111. ThemeUtils.colorHorizontalSeekBar(seeker, getContext());
  112. seeker.setOnSeekBarChangeListener(this);
  113. } else {
  114. ThemeUtils.colorHorizontalProgressBar(progressBar, ThemeUtils.primaryAccentColor(getContext()));
  115. }
  116. progressBar.setMax(1000);
  117. }
  118. endTime = v.findViewById(R.id.totalTimeText);
  119. currentTime = v.findViewById(R.id.currentTimeText);
  120. }
  121. /**
  122. * Disable pause or seek buttons if the stream cannot be paused or seeked.
  123. * This requires the control interface to be a MediaPlayerControlExt
  124. */
  125. private void disableUnsupportedButtons() {
  126. try {
  127. if (pauseButton != null && !playerControl.canPause()) {
  128. pauseButton.setEnabled(false);
  129. }
  130. if (rewindButton != null && !playerControl.canSeekBackward()) {
  131. rewindButton.setEnabled(false);
  132. }
  133. if (forwardButton != null && !playerControl.canSeekForward()) {
  134. forwardButton.setEnabled(false);
  135. }
  136. } catch (IncompatibleClassChangeError ex) {
  137. // We were given an old version of the interface, that doesn't have
  138. // the canPause/canSeekXYZ methods. This is OK, it just means we
  139. // assume the media can be paused and seeked, and so we don't disable
  140. // the buttons.
  141. Log_OC.i(TAG, "Old media interface detected");
  142. }
  143. }
  144. private Handler handler = new Handler() {
  145. @Override
  146. public void handleMessage(Message msg) {
  147. int pos;
  148. if (msg.what == SHOW_PROGRESS) {
  149. updatePausePlay();
  150. pos = setProgress();
  151. if (!isDragging) {
  152. msg = obtainMessage(SHOW_PROGRESS);
  153. sendMessageDelayed(msg, 1000 - (pos % 1000));
  154. }
  155. }
  156. }
  157. };
  158. private String formatTime(int timeMs) {
  159. int totalSeconds = timeMs / 1000;
  160. int seconds = totalSeconds % 60;
  161. int minutes = (totalSeconds / 60) % 60;
  162. int hours = totalSeconds / 3600;
  163. final StringBuilder mFormatBuilder = new StringBuilder();
  164. final Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
  165. if (hours > 0) {
  166. return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
  167. } else {
  168. return mFormatter.format("%02d:%02d", minutes, seconds).toString();
  169. }
  170. }
  171. private int setProgress() {
  172. if (playerControl == null || isDragging) {
  173. return 0;
  174. }
  175. int position = playerControl.getCurrentPosition();
  176. int duration = playerControl.getDuration();
  177. if (progressBar != null) {
  178. if (duration > 0) {
  179. // use long to avoid overflow
  180. long pos = 1000L * position / duration;
  181. progressBar.setProgress((int) pos);
  182. }
  183. int percent = playerControl.getBufferPercentage();
  184. progressBar.setSecondaryProgress(percent * 10);
  185. }
  186. if (endTime != null) {
  187. String endTime = duration > 0 ? formatTime(duration) : "--:--";
  188. this.endTime.setText(endTime);
  189. }
  190. if (currentTime != null) {
  191. currentTime.setText(formatTime(position));
  192. }
  193. return position;
  194. }
  195. @Override
  196. public boolean dispatchKeyEvent(KeyEvent event) {
  197. int keyCode = event.getKeyCode();
  198. final boolean uniqueDown = event.getRepeatCount() == 0
  199. && event.getAction() == KeyEvent.ACTION_DOWN;
  200. if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
  201. || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
  202. || keyCode == KeyEvent.KEYCODE_SPACE) {
  203. if (uniqueDown) {
  204. doPauseResume();
  205. //show(sDefaultTimeout);
  206. if (pauseButton != null) {
  207. pauseButton.requestFocus();
  208. }
  209. }
  210. return true;
  211. } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
  212. if (uniqueDown && !playerControl.isPlaying()) {
  213. playerControl.start();
  214. updatePausePlay();
  215. }
  216. return true;
  217. } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
  218. || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
  219. if (uniqueDown && playerControl.isPlaying()) {
  220. playerControl.pause();
  221. updatePausePlay();
  222. }
  223. return true;
  224. }
  225. return super.dispatchKeyEvent(event);
  226. }
  227. public void updatePausePlay() {
  228. if (root == null || pauseButton == null) {
  229. return;
  230. }
  231. if (playerControl.isPlaying()) {
  232. pauseButton.setImageResource(android.R.drawable.ic_media_pause);
  233. } else {
  234. pauseButton.setImageResource(android.R.drawable.ic_media_play);
  235. }
  236. final boolean canSeekFfd = playerControl.canSeekForward();
  237. if (canSeekFfd) {
  238. forwardButton.setVisibility(View.VISIBLE);
  239. } else {
  240. forwardButton.setVisibility(View.INVISIBLE);
  241. }
  242. final boolean canSeekBwd = playerControl.canSeekBackward();
  243. if (canSeekBwd) {
  244. rewindButton.setVisibility(View.VISIBLE);
  245. } else {
  246. rewindButton.setVisibility(View.INVISIBLE);
  247. }
  248. }
  249. private void doPauseResume() {
  250. if (playerControl.isPlaying()) {
  251. playerControl.pause();
  252. } else {
  253. playerControl.start();
  254. }
  255. updatePausePlay();
  256. }
  257. @Override
  258. public void setEnabled(boolean enabled) {
  259. if (pauseButton != null) {
  260. pauseButton.setEnabled(enabled);
  261. }
  262. if (forwardButton != null) {
  263. forwardButton.setEnabled(enabled);
  264. }
  265. if (rewindButton != null) {
  266. rewindButton.setEnabled(enabled);
  267. }
  268. if (progressBar != null) {
  269. progressBar.setEnabled(enabled);
  270. }
  271. disableUnsupportedButtons();
  272. super.setEnabled(enabled);
  273. }
  274. @Override
  275. public void onClick(View v) {
  276. int pos;
  277. boolean playing = playerControl.isPlaying();
  278. switch (v.getId()) {
  279. case R.id.playBtn:
  280. doPauseResume();
  281. break;
  282. case R.id.rewindBtn:
  283. pos = playerControl.getCurrentPosition();
  284. pos -= 5000;
  285. playerControl.seekTo(pos);
  286. if (!playing) {
  287. playerControl.pause(); // necessary in some 2.3.x devices
  288. }
  289. setProgress();
  290. break;
  291. case R.id.forwardBtn:
  292. pos = playerControl.getCurrentPosition();
  293. pos += 15000;
  294. playerControl.seekTo(pos);
  295. if (!playing) {
  296. playerControl.pause(); // necessary in some 2.3.x devices
  297. }
  298. setProgress();
  299. break;
  300. default:
  301. // do nothing
  302. break;
  303. }
  304. }
  305. @Override
  306. public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
  307. if (!fromUser) {
  308. // We're not interested in programmatically generated changes to
  309. // the progress bar's position.
  310. return;
  311. }
  312. long duration = playerControl.getDuration();
  313. long newPosition = (duration * progress) / 1000L;
  314. playerControl.seekTo((int) newPosition);
  315. if (currentTime != null) {
  316. currentTime.setText(formatTime((int) newPosition));
  317. }
  318. }
  319. /**
  320. * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb.
  321. *
  322. * Will be followed by several onProgressChanged notifications.
  323. */
  324. @Override
  325. public void onStartTrackingTouch(SeekBar seekBar) {
  326. isDragging = true; // monitors the duration of dragging
  327. handler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging
  328. }
  329. /**
  330. * Called in devices with touchpad when the user finishes the adjusting of the seekbar.
  331. */
  332. @Override
  333. public void onStopTrackingTouch(SeekBar seekBar) {
  334. isDragging = false;
  335. setProgress();
  336. updatePausePlay();
  337. handler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress
  338. }
  339. @Override
  340. public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
  341. super.onInitializeAccessibilityEvent(event);
  342. event.setClassName(MediaControlView.class.getName());
  343. }
  344. @Override
  345. public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
  346. super.onInitializeAccessibilityNodeInfo(info);
  347. info.setClassName(MediaControlView.class.getName());
  348. }
  349. }