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