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