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