AudioPlayer.java

  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}