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            final MediaPlayer player = AudioPlayer.player;
317            if (fromUser && message == AudioPlayer.currentlyPlayingMessage && player != null) {
318                float percent = progress / 100f;
319                int duration = player.getDuration();
320                int seekTo = Math.round(duration * percent);
321                player.seekTo(seekTo);
322            }
323        }
324    }
325
326    @Override
327    public void onStartTrackingTouch(SeekBar seekBar) {}
328
329    @Override
330    public void onStopTrackingTouch(SeekBar seekBar) {}
331
332    public void stop() {
333        synchronized (AudioPlayer.LOCK) {
334            stopRefresher(false);
335            if (AudioPlayer.player != null) {
336                stopCurrent();
337            }
338            AudioPlayer.currentlyPlayingMessage = null;
339            sensorManager.unregisterListener(this);
340            if (wakeLock != null && wakeLock.isHeld()) {
341                wakeLock.release();
342            }
343            wakeLock = null;
344        }
345    }
346
347    private void stopRefresher(boolean runOnceMore) {
348        this.handler.removeCallbacks(this);
349        if (runOnceMore) {
350            this.handler.post(this);
351        }
352    }
353
354    public void unregisterListener() {
355        if (sensorManager != null) {
356            sensorManager.unregisterListener(this);
357        }
358    }
359
360    @Override
361    public void run() {
362        synchronized (AudioPlayer.LOCK) {
363            if (AudioPlayer.player != null) {
364                boolean renew = false;
365                final int current = player.getCurrentPosition();
366                final int duration = player.getDuration();
367                for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
368                    renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
369                }
370                if (renew && AudioPlayer.player.isPlaying()) {
371                    handler.postDelayed(this, REFRESH_INTERVAL);
372                }
373            }
374        }
375    }
376
377    private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
378        if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
379            return false;
380        }
381        final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
382        if (duration <= 0) {
383            viewHolder.progress.setProgress(100);
384        } else {
385            final var progress = current * 100L / duration;
386            viewHolder.progress.setProgress(Math.min(Ints.saturatedCast(progress), 100));
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}