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 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}