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