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 final MediaPlayer player = AudioPlayer.player;
291 if (fromUser && message == AudioPlayer.currentlyPlayingMessage && player != null) {
292 float percent = progress / 100f;
293 int duration = player.getDuration();
294 int seekTo = Math.round(duration * percent);
295 player.seekTo(seekTo);
296 }
297 }
298 }
299
300 @Override
301 public void onStartTrackingTouch(SeekBar seekBar) {
302
303 }
304
305 @Override
306 public void onStopTrackingTouch(SeekBar seekBar) {
307
308 }
309
310 public void stop() {
311 synchronized (AudioPlayer.LOCK) {
312 stopRefresher(false);
313 if (AudioPlayer.player != null) {
314 stopCurrent();
315 }
316 AudioPlayer.currentlyPlayingMessage = null;
317 sensorManager.unregisterListener(this);
318 if (wakeLock != null && wakeLock.isHeld()) {
319 wakeLock.release();
320 }
321 wakeLock = null;
322 }
323 }
324
325 private void stopRefresher(boolean runOnceMore) {
326 this.handler.removeCallbacks(this);
327 if (runOnceMore) {
328 this.handler.post(this);
329 }
330 }
331
332 public void unregisterListener() {
333 if (sensorManager != null) {
334 sensorManager.unregisterListener(this);
335 }
336 }
337
338 @Override
339 public void run() {
340 synchronized (AudioPlayer.LOCK) {
341 if (AudioPlayer.player != null) {
342 boolean renew = false;
343 final int current = player.getCurrentPosition();
344 final int duration = player.getDuration();
345 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
346 renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
347 }
348 if (renew && AudioPlayer.player.isPlaying()) {
349 handler.postDelayed(this, REFRESH_INTERVAL);
350 }
351 }
352 }
353 }
354
355 private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
356 if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
357 return false;
358 }
359 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
360 if (duration <= 0) {
361 viewHolder.progress.setProgress(100);
362 } else {
363 viewHolder.progress.setProgress(current * 100 / duration);
364 }
365 viewHolder.runtime.setText(String.format("%s / %s", formatTime(current), formatTime(duration)));
366 return true;
367 }
368
369 @Override
370 public void onSensorChanged(SensorEvent event) {
371 if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) {
372 return;
373 }
374 if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) {
375 return;
376 }
377 final int streamType;
378 if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
379 streamType = AudioManager.STREAM_VOICE_CALL;
380 } else {
381 streamType = AudioManager.STREAM_MUSIC;
382 }
383 messageAdapter.setVolumeControl(streamType);
384 double position = AudioPlayer.player.getCurrentPosition();
385 double duration = AudioPlayer.player.getDuration();
386 double progress = position / duration;
387 if (AudioPlayer.player.getAudioStreamType() != streamType) {
388 synchronized (AudioPlayer.LOCK) {
389 AudioPlayer.player.stop();
390 AudioPlayer.player.release();
391 AudioPlayer.player = null;
392 try {
393 ViewHolder currentViewHolder = getCurrentViewHolder();
394 if (currentViewHolder != null) {
395 play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
396 }
397 } catch (Exception e) {
398 Log.w(Config.LOGTAG, e);
399 }
400 }
401 }
402 }
403
404 @Override
405 public void onAccuracyChanged(Sensor sensor, int i) {
406 }
407
408 private void acquireProximityWakeLock() {
409 synchronized (AudioPlayer.LOCK) {
410 if (wakeLock != null) {
411 wakeLock.acquire();
412 }
413 }
414 }
415
416 private void releaseProximityWakeLock() {
417 synchronized (AudioPlayer.LOCK) {
418 if (wakeLock != null && wakeLock.isHeld()) {
419 wakeLock.release();
420 }
421 }
422 messageAdapter.setVolumeControl(AudioManager.STREAM_MUSIC);
423 }
424
425 private ViewHolder getCurrentViewHolder() {
426 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
427 final Message message = (Message) audioPlayer.get().getTag();
428 if (message == currentlyPlayingMessage) {
429 return ViewHolder.get(audioPlayer.get());
430 }
431 }
432 return null;
433 }
434
435 public static class ViewHolder {
436 private TextView runtime;
437 private SeekBar progress;
438 private ImageButton playPause;
439 private boolean darkBackground = false;
440
441 public static ViewHolder get(RelativeLayout audioPlayer) {
442 ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
443 if (viewHolder == null) {
444 viewHolder = new ViewHolder();
445 viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
446 viewHolder.progress = audioPlayer.findViewById(R.id.progress);
447 viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
448 audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
449 }
450 return viewHolder;
451 }
452
453 public void setDarkBackground(boolean darkBackground) {
454 this.darkBackground = darkBackground;
455 }
456 }
457}