AudioPlayer.java

  1package eu.siacs.conversations.ui.service;
  2
  3import android.Manifest;
  4import android.content.pm.PackageManager;
  5import android.content.res.ColorStateList;
  6import android.media.Image;
  7import android.media.MediaPlayer;
  8import android.os.Build;
  9import android.os.Handler;
 10import android.support.v4.app.ActivityCompat;
 11import android.support.v4.content.ContextCompat;
 12import android.view.View;
 13import android.widget.ImageButton;
 14import android.widget.RelativeLayout;
 15import android.widget.SeekBar;
 16import android.widget.TextView;
 17
 18import java.lang.ref.WeakReference;
 19import java.util.Locale;
 20
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.entities.Message;
 23import eu.siacs.conversations.ui.ConversationActivity;
 24import eu.siacs.conversations.ui.adapter.MessageAdapter;
 25import eu.siacs.conversations.ui.util.PendingItem;
 26import eu.siacs.conversations.utils.WeakReferenceSet;
 27
 28public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable {
 29
 30	private static final int REFRESH_INTERVAL = 250;
 31	private static final Object LOCK = new Object();
 32	private static MediaPlayer player = null;
 33	private static Message currentlyPlayingMessage = null;
 34	private final MessageAdapter messageAdapter;
 35	private final WeakReferenceSet<RelativeLayout> audioPlayerLayouts = new WeakReferenceSet<>();
 36
 37	private final PendingItem<WeakReference<ImageButton>> pendingOnClickView = new PendingItem<>();
 38
 39	private final Handler handler = new Handler();
 40
 41	public AudioPlayer(MessageAdapter adapter) {
 42		this.messageAdapter = adapter;
 43		synchronized (AudioPlayer.LOCK) {
 44			if (AudioPlayer.player != null) {
 45				AudioPlayer.player.setOnCompletionListener(this);
 46			}
 47		}
 48	}
 49
 50	private static String formatTime(int ms) {
 51		return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59));
 52	}
 53
 54	public void init(RelativeLayout audioPlayer, Message message) {
 55		synchronized (AudioPlayer.LOCK) {
 56			audioPlayer.setTag(message);
 57			if (init(ViewHolder.get(audioPlayer), message)) {
 58				this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
 59				this.stopRefresher(true);
 60			} else {
 61				this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer);
 62			}
 63		}
 64	}
 65
 66	private boolean init(ViewHolder viewHolder, Message message) {
 67		if (viewHolder.darkBackground) {
 68			viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
 69		} else {
 70			viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
 71		}
 72		viewHolder.progress.setOnSeekBarChangeListener(this);
 73		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 74			ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.bubble);
 75			viewHolder.progress.setThumbTintList(color);
 76			viewHolder.progress.setProgressTintList(color);
 77		}
 78		viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
 79		viewHolder.playPause.setOnClickListener(this);
 80		if (message == currentlyPlayingMessage) {
 81			if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
 82				viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
 83				viewHolder.progress.setEnabled(true);
 84			} else {
 85				viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
 86				viewHolder.progress.setEnabled(false);
 87			}
 88			return true;
 89		} else {
 90			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
 91			viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
 92			viewHolder.progress.setProgress(0);
 93			viewHolder.progress.setEnabled(false);
 94			return false;
 95		}
 96	}
 97
 98	@Override
 99	public synchronized void onClick(View v) {
100		if (v.getId() == R.id.play_pause) {
101			synchronized (LOCK) {
102				startStop((ImageButton) v);
103			}
104		}
105	}
106
107	private void startStop(ImageButton playPause) {
108		if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
109			pendingOnClickView.push(new WeakReference<>(playPause));
110			ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationActivity.REQUEST_PLAY_PAUSE);
111			return;
112		}
113		final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent();
114		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
115		final Message message = (Message) audioPlayer.getTag();
116		if (startStop(viewHolder, message)) {
117			this.audioPlayerLayouts.clear();
118			this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
119			stopRefresher(true);
120		}
121	}
122
123	private boolean playPauseCurrent(ViewHolder viewHolder) {
124		viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
125		if (player.isPlaying()) {
126			viewHolder.progress.setEnabled(false);
127			player.pause();
128			messageAdapter.flagScreenOff();
129			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
130		} else {
131			viewHolder.progress.setEnabled(true);
132			player.start();
133			messageAdapter.flagScreenOn();
134			this.stopRefresher(true);
135			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
136		}
137		return false;
138	}
139
140	private boolean play(ViewHolder viewHolder, Message message) {
141		AudioPlayer.player = new MediaPlayer();
142		try {
143			AudioPlayer.currentlyPlayingMessage = message;
144			AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
145			AudioPlayer.player.setOnCompletionListener(this);
146			AudioPlayer.player.prepare();
147			AudioPlayer.player.start();
148			messageAdapter.flagScreenOn();
149			viewHolder.progress.setEnabled(true);
150			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
151			return true;
152		} catch (Exception e) {
153			messageAdapter.flagScreenOff();
154			AudioPlayer.currentlyPlayingMessage = null;
155			return false;
156		}
157	}
158
159	public void startStopPending() {
160		WeakReference<ImageButton> reference = pendingOnClickView.pop();
161		if (reference != null) {
162			ImageButton imageButton = reference.get();
163			if (imageButton != null) {
164				startStop(imageButton);
165			}
166		}
167	}
168
169	private boolean startStop(ViewHolder viewHolder, Message message) {
170		if (message == currentlyPlayingMessage && player != null) {
171			return playPauseCurrent(viewHolder);
172		}
173		if (AudioPlayer.player != null) {
174			stopCurrent();
175		}
176		return play(viewHolder, message);
177	}
178
179	private void stopCurrent() {
180		if (AudioPlayer.player.isPlaying()) {
181			AudioPlayer.player.stop();
182		}
183		AudioPlayer.player.release();
184		messageAdapter.flagScreenOff();
185		AudioPlayer.player = null;
186		resetPlayerUi();
187	}
188
189	private void resetPlayerUi() {
190		for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
191			resetPlayerUi(audioPlayer.get());
192		}
193	}
194
195	private void resetPlayerUi(RelativeLayout audioPlayer) {
196		if (audioPlayer == null) {
197			return;
198		}
199		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
200		final Message message = (Message) audioPlayer.getTag();
201		viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
202		if (message != null) {
203			viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
204		}
205		viewHolder.progress.setProgress(0);
206		viewHolder.progress.setEnabled(false);
207	}
208
209	@Override
210	public void onCompletion(MediaPlayer mediaPlayer) {
211		synchronized (AudioPlayer.LOCK) {
212			this.stopRefresher(false);
213			if (AudioPlayer.player == mediaPlayer) {
214				AudioPlayer.currentlyPlayingMessage = null;
215				AudioPlayer.player = null;
216			}
217			mediaPlayer.release();
218			messageAdapter.flagScreenOff();
219			resetPlayerUi();
220		}
221	}
222
223	@Override
224	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
225		synchronized (AudioPlayer.LOCK) {
226			final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent();
227			final Message message = (Message) audioPlayer.getTag();
228			if (fromUser && message == AudioPlayer.currentlyPlayingMessage) {
229				float percent = progress / 100f;
230				int duration = AudioPlayer.player.getDuration();
231				int seekTo = Math.round(duration * percent);
232				AudioPlayer.player.seekTo(seekTo);
233			}
234		}
235	}
236
237	@Override
238	public void onStartTrackingTouch(SeekBar seekBar) {
239
240	}
241
242	@Override
243	public void onStopTrackingTouch(SeekBar seekBar) {
244
245	}
246
247	public void stop() {
248		synchronized (AudioPlayer.LOCK) {
249			stopRefresher(false);
250			if (AudioPlayer.player != null) {
251				stopCurrent();
252			}
253			AudioPlayer.currentlyPlayingMessage = null;
254		}
255	}
256
257	private void stopRefresher(boolean runOnceMore) {
258		this.handler.removeCallbacks(this);
259		if (runOnceMore) {
260			this.handler.post(this);
261		}
262	}
263
264	@Override
265	public void run() {
266		synchronized (AudioPlayer.LOCK) {
267			if (AudioPlayer.player != null) {
268				boolean renew = false;
269				final int current = player.getCurrentPosition();
270				final int duration = player.getDuration();
271				for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
272					renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
273				}
274				if (renew && AudioPlayer.player.isPlaying()) {
275					handler.postDelayed(this, REFRESH_INTERVAL);
276				}
277			}
278		}
279	}
280
281	private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
282		if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
283			return false;
284		}
285		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
286		viewHolder.progress.setProgress(current * 100 / duration);
287		viewHolder.runtime.setText(formatTime(current) + " / " + formatTime(duration));
288		return true;
289	}
290
291	public static class ViewHolder {
292		private TextView runtime;
293		private SeekBar progress;
294		private ImageButton playPause;
295		private boolean darkBackground = false;
296
297		public static ViewHolder get(RelativeLayout audioPlayer) {
298			ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
299			if (viewHolder == null) {
300				viewHolder = new ViewHolder();
301				viewHolder.runtime = (TextView) audioPlayer.findViewById(R.id.runtime);
302				viewHolder.progress = (SeekBar) audioPlayer.findViewById(R.id.progress);
303				viewHolder.playPause = (ImageButton) audioPlayer.findViewById(R.id.play_pause);
304				audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
305			}
306			return viewHolder;
307		}
308
309		public void setDarkBackground(boolean darkBackground) {
310			this.darkBackground = darkBackground;
311		}
312	}
313}