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