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