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