AudioPlayer.java

  1package eu.siacs.conversations.ui.service;
  2
  3import android.Manifest;
  4import android.content.Context;
  5import android.content.pm.PackageManager;
  6import android.content.res.ColorStateList;
  7import android.hardware.Sensor;
  8import android.hardware.SensorEvent;
  9import android.hardware.SensorEventListener;
 10import android.hardware.SensorManager;
 11import android.media.AudioManager;
 12import android.os.Build;
 13import android.os.Handler;
 14import android.os.PowerManager;
 15import android.support.v4.app.ActivityCompat;
 16import android.support.v4.content.ContextCompat;
 17import android.util.Log;
 18import android.view.View;
 19import android.widget.ImageButton;
 20import android.widget.RelativeLayout;
 21import android.widget.SeekBar;
 22import android.widget.TextView;
 23
 24import java.lang.ref.WeakReference;
 25import java.util.Locale;
 26
 27import eu.siacs.conversations.Config;
 28import eu.siacs.conversations.R;
 29import eu.siacs.conversations.entities.Message;
 30import eu.siacs.conversations.services.MediaPlayer;
 31import eu.siacs.conversations.ui.ConversationsActivity;
 32import eu.siacs.conversations.ui.adapter.MessageAdapter;
 33import eu.siacs.conversations.ui.util.PendingItem;
 34import eu.siacs.conversations.utils.WeakReferenceSet;
 35
 36public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable, SensorEventListener {
 37
 38	private static final int REFRESH_INTERVAL = 250;
 39	private static final Object LOCK = new Object();
 40	private static MediaPlayer player = null;
 41	private static Message currentlyPlayingMessage = null;
 42	private final MessageAdapter messageAdapter;
 43	private final WeakReferenceSet<RelativeLayout> audioPlayerLayouts = new WeakReferenceSet<>();
 44	private final SensorManager sensorManager;
 45	private final Sensor proximitySensor;
 46	private static PowerManager.WakeLock wakeLock;
 47
 48	private final PendingItem<WeakReference<ImageButton>> pendingOnClickView = new PendingItem<>();
 49
 50	private final Handler handler = new Handler();
 51
 52	public AudioPlayer(MessageAdapter adapter) {
 53		final Context context = adapter.getContext();
 54		this.messageAdapter = adapter;
 55		this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
 56		this.proximitySensor = this.sensorManager == null ? null : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
 57		if (Build.VERSION.SDK_INT >= 21) {
 58			synchronized (AudioPlayer.LOCK) {
 59				if (AudioPlayer.wakeLock == null) {
 60					final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
 61					AudioPlayer.wakeLock = powerManager == null ? null : powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, AudioPlayer.class.getSimpleName());
 62					AudioPlayer.wakeLock.setReferenceCounted(false);
 63				}
 64			}
 65		} else {
 66			AudioPlayer.wakeLock = null;
 67		}
 68		synchronized (AudioPlayer.LOCK) {
 69			if (AudioPlayer.player != null) {
 70				AudioPlayer.player.setOnCompletionListener(this);
 71				if (AudioPlayer.player.isPlaying() && sensorManager != null) {
 72					sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
 73				}
 74			}
 75		}
 76	}
 77
 78	private static String formatTime(int ms) {
 79		return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59));
 80	}
 81
 82	public void init(RelativeLayout audioPlayer, Message message) {
 83		synchronized (AudioPlayer.LOCK) {
 84			audioPlayer.setTag(message);
 85			if (init(ViewHolder.get(audioPlayer), message)) {
 86				this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
 87				this.stopRefresher(true);
 88			} else {
 89				this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer);
 90			}
 91		}
 92	}
 93
 94	private boolean init(ViewHolder viewHolder, Message message) {
 95		if (viewHolder.darkBackground) {
 96			viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
 97		} else {
 98			viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
 99		}
100		viewHolder.progress.setOnSeekBarChangeListener(this);
101		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
102			ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.green700_desaturated);
103			viewHolder.progress.setThumbTintList(color);
104			viewHolder.progress.setProgressTintList(color);
105		}
106		viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
107		viewHolder.playPause.setOnClickListener(this);
108		if (message == currentlyPlayingMessage) {
109			if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
110				viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
111				viewHolder.progress.setEnabled(true);
112			} else {
113				viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
114				viewHolder.progress.setEnabled(false);
115			}
116			return true;
117		} else {
118			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
119			viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
120			viewHolder.progress.setProgress(0);
121			viewHolder.progress.setEnabled(false);
122			return false;
123		}
124	}
125
126	@Override
127	public synchronized void onClick(View v) {
128		if (v.getId() == R.id.play_pause) {
129			synchronized (LOCK) {
130				startStop((ImageButton) v);
131			}
132		}
133	}
134
135	private void startStop(ImageButton playPause) {
136		if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
137			pendingOnClickView.push(new WeakReference<>(playPause));
138			ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
139			return;
140		}
141		final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent();
142		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
143		final Message message = (Message) audioPlayer.getTag();
144		if (startStop(viewHolder, message)) {
145			this.audioPlayerLayouts.clear();
146			this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
147			stopRefresher(true);
148		}
149	}
150
151	private boolean playPauseCurrent(ViewHolder viewHolder) {
152		viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
153		if (player.isPlaying()) {
154			viewHolder.progress.setEnabled(false);
155			player.pause();
156			messageAdapter.flagScreenOff();
157			releaseProximityWakeLock();
158			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
159		} else {
160			viewHolder.progress.setEnabled(true);
161			player.start();
162			messageAdapter.flagScreenOn();
163			acquireProximityWakeLock();
164			this.stopRefresher(true);
165			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
166		}
167		return false;
168	}
169
170	private void play(ViewHolder viewHolder, Message message, boolean earpiece, double progress) {
171		if (play(viewHolder, message, earpiece)) {
172			AudioPlayer.player.seekTo((int) (AudioPlayer.player.getDuration() * progress));
173		}
174	}
175
176	private boolean play(ViewHolder viewHolder, Message message, boolean earpiece) {
177		AudioPlayer.player = new MediaPlayer();
178		try {
179			AudioPlayer.currentlyPlayingMessage = message;
180			AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
181			AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
182			AudioPlayer.player.setOnCompletionListener(this);
183			AudioPlayer.player.prepare();
184			AudioPlayer.player.start();
185			messageAdapter.flagScreenOn();
186			acquireProximityWakeLock();
187			viewHolder.progress.setEnabled(true);
188			viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
189			sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
190			return true;
191		} catch (Exception e) {
192			messageAdapter.flagScreenOff();
193			releaseProximityWakeLock();
194			AudioPlayer.currentlyPlayingMessage = null;
195			sensorManager.unregisterListener(this);
196			return false;
197		}
198	}
199
200	public void startStopPending() {
201		WeakReference<ImageButton> reference = pendingOnClickView.pop();
202		if (reference != null) {
203			ImageButton imageButton = reference.get();
204			if (imageButton != null) {
205				startStop(imageButton);
206			}
207		}
208	}
209
210	private boolean startStop(ViewHolder viewHolder, Message message) {
211		if (message == currentlyPlayingMessage && player != null) {
212			return playPauseCurrent(viewHolder);
213		}
214		if (AudioPlayer.player != null) {
215			stopCurrent();
216		}
217		return play(viewHolder, message, false);
218	}
219
220	private void stopCurrent() {
221		if (AudioPlayer.player.isPlaying()) {
222			AudioPlayer.player.stop();
223		}
224		AudioPlayer.player.release();
225		messageAdapter.flagScreenOff();
226		releaseProximityWakeLock();
227		AudioPlayer.player = null;
228		resetPlayerUi();
229	}
230
231	private void resetPlayerUi() {
232		for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
233			resetPlayerUi(audioPlayer.get());
234		}
235	}
236
237	private void resetPlayerUi(RelativeLayout audioPlayer) {
238		if (audioPlayer == null) {
239			return;
240		}
241		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
242		final Message message = (Message) audioPlayer.getTag();
243		viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
244		if (message != null) {
245			viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
246		}
247		viewHolder.progress.setProgress(0);
248		viewHolder.progress.setEnabled(false);
249	}
250
251	@Override
252	public void onCompletion(android.media.MediaPlayer mediaPlayer) {
253		synchronized (AudioPlayer.LOCK) {
254			this.stopRefresher(false);
255			if (AudioPlayer.player == mediaPlayer) {
256				AudioPlayer.currentlyPlayingMessage = null;
257				AudioPlayer.player = null;
258			}
259			mediaPlayer.release();
260			messageAdapter.flagScreenOff();
261			releaseProximityWakeLock();
262			resetPlayerUi();
263			sensorManager.unregisterListener(this);
264		}
265	}
266
267	@Override
268	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
269		synchronized (AudioPlayer.LOCK) {
270			final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent();
271			final Message message = (Message) audioPlayer.getTag();
272			if (fromUser && message == AudioPlayer.currentlyPlayingMessage) {
273				float percent = progress / 100f;
274				int duration = AudioPlayer.player.getDuration();
275				int seekTo = Math.round(duration * percent);
276				AudioPlayer.player.seekTo(seekTo);
277			}
278		}
279	}
280
281	@Override
282	public void onStartTrackingTouch(SeekBar seekBar) {
283
284	}
285
286	@Override
287	public void onStopTrackingTouch(SeekBar seekBar) {
288
289	}
290
291	public void stop() {
292		synchronized (AudioPlayer.LOCK) {
293			stopRefresher(false);
294			if (AudioPlayer.player != null) {
295				stopCurrent();
296			}
297			AudioPlayer.currentlyPlayingMessage = null;
298			sensorManager.unregisterListener(this);
299			if (wakeLock != null && wakeLock.isHeld()) {
300				wakeLock.release();
301			}
302			wakeLock = null;
303		}
304	}
305
306	private void stopRefresher(boolean runOnceMore) {
307		this.handler.removeCallbacks(this);
308		if (runOnceMore) {
309			this.handler.post(this);
310		}
311	}
312
313	public void unregisterListener() {
314		if (sensorManager != null) {
315			sensorManager.unregisterListener(this);
316		}
317	}
318
319	@Override
320	public void run() {
321		synchronized (AudioPlayer.LOCK) {
322			if (AudioPlayer.player != null) {
323				boolean renew = false;
324				final int current = player.getCurrentPosition();
325				final int duration = player.getDuration();
326				for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
327					renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
328				}
329				if (renew && AudioPlayer.player.isPlaying()) {
330					handler.postDelayed(this, REFRESH_INTERVAL);
331				}
332			}
333		}
334	}
335
336	private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
337		if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
338			return false;
339		}
340		final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
341		viewHolder.progress.setProgress(current * 100 / duration);
342		viewHolder.runtime.setText(formatTime(current) + " / " + formatTime(duration));
343		return true;
344	}
345
346	@Override
347	public void onSensorChanged(SensorEvent event) {
348		if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) {
349			return;
350		}
351		if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) {
352			return;
353		}
354		int streamType;
355		if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
356			streamType = AudioManager.STREAM_VOICE_CALL;
357		} else {
358			streamType = AudioManager.STREAM_MUSIC;
359		}
360		double position = AudioPlayer.player.getCurrentPosition();
361		double duration = AudioPlayer.player.getDuration();
362		double progress = position / duration;
363		if (AudioPlayer.player.getAudioStreamType() != streamType) {
364			synchronized (AudioPlayer.LOCK) {
365				AudioPlayer.player.stop();
366				AudioPlayer.player.release();
367				AudioPlayer.player = null;
368				try {
369					ViewHolder currentViewHolder = getCurrentViewHolder();
370					if (currentViewHolder != null) {
371						play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
372					}
373				} catch (Exception e) {
374					Log.w(Config.LOGTAG, e);
375				}
376			}
377		}
378	}
379
380	@Override
381	public void onAccuracyChanged(Sensor sensor, int i) {
382	}
383
384	private void acquireProximityWakeLock() {
385		synchronized (AudioPlayer.LOCK) {
386			if (wakeLock != null) {
387				wakeLock.acquire();
388			}
389		}
390	}
391
392	private void releaseProximityWakeLock() {
393		synchronized (AudioPlayer.LOCK) {
394			if (wakeLock != null && wakeLock.isHeld()) {
395				wakeLock.release();
396			}
397		}
398	}
399
400	private ViewHolder getCurrentViewHolder() {
401		for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
402			final Message message = (Message) audioPlayer.get().getTag();
403			if (message == currentlyPlayingMessage) {
404				return ViewHolder.get(audioPlayer.get());
405			}
406		}
407		return null;
408	}
409
410	public static class ViewHolder {
411		private TextView runtime;
412		private SeekBar progress;
413		private ImageButton playPause;
414		private boolean darkBackground = false;
415
416		public static ViewHolder get(RelativeLayout audioPlayer) {
417			ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
418			if (viewHolder == null) {
419				viewHolder = new ViewHolder();
420				viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
421				viewHolder.progress = audioPlayer.findViewById(R.id.progress);
422				viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
423				audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
424			}
425			return viewHolder;
426		}
427
428		public void setDarkBackground(boolean darkBackground) {
429			this.darkBackground = darkBackground;
430		}
431	}
432}