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.support.v4.app.ActivityCompat;
 16import android.support.v4.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        if (message == currentlyPlayingMessage) {
116            if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
117                viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
118                viewHolder.progress.setEnabled(true);
119            } else {
120                viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
121                viewHolder.progress.setEnabled(false);
122            }
123            return true;
124        } else {
125            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
126            viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
127            viewHolder.progress.setProgress(0);
128            viewHolder.progress.setEnabled(false);
129            return false;
130        }
131    }
132
133    @Override
134    public synchronized void onClick(View v) {
135        if (v.getId() == R.id.play_pause) {
136            synchronized (LOCK) {
137                startStop((ImageButton) v);
138            }
139        }
140    }
141
142    private void startStop(ImageButton playPause) {
143        if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
144            pendingOnClickView.push(new WeakReference<>(playPause));
145            ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
146            return;
147        }
148        initializeProximityWakeLock(playPause.getContext());
149        final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent();
150        final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
151        final Message message = (Message) audioPlayer.getTag();
152        if (startStop(viewHolder, message)) {
153            this.audioPlayerLayouts.clear();
154            this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
155            stopRefresher(true);
156        }
157    }
158
159    private boolean playPauseCurrent(ViewHolder viewHolder) {
160        viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
161        if (player.isPlaying()) {
162            viewHolder.progress.setEnabled(false);
163            player.pause();
164            messageAdapter.flagScreenOff();
165            releaseProximityWakeLock();
166            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
167        } else {
168            viewHolder.progress.setEnabled(true);
169            player.start();
170            messageAdapter.flagScreenOn();
171            acquireProximityWakeLock();
172            this.stopRefresher(true);
173            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
174        }
175        return false;
176    }
177
178    private void play(ViewHolder viewHolder, Message message, boolean earpiece, double progress) {
179        if (play(viewHolder, message, earpiece)) {
180            AudioPlayer.player.seekTo((int) (AudioPlayer.player.getDuration() * progress));
181        }
182    }
183
184    private boolean play(ViewHolder viewHolder, Message message, boolean earpiece) {
185        AudioPlayer.player = new MediaPlayer();
186        try {
187            AudioPlayer.currentlyPlayingMessage = message;
188            AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
189            AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
190            AudioPlayer.player.setOnCompletionListener(this);
191            AudioPlayer.player.prepare();
192            AudioPlayer.player.start();
193            messageAdapter.flagScreenOn();
194            acquireProximityWakeLock();
195            viewHolder.progress.setEnabled(true);
196            viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
197            sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
198            return true;
199        } catch (Exception e) {
200            messageAdapter.flagScreenOff();
201            releaseProximityWakeLock();
202            AudioPlayer.currentlyPlayingMessage = null;
203            sensorManager.unregisterListener(this);
204            return false;
205        }
206    }
207
208    public void startStopPending() {
209        WeakReference<ImageButton> reference = pendingOnClickView.pop();
210        if (reference != null) {
211            ImageButton imageButton = reference.get();
212            if (imageButton != null) {
213                startStop(imageButton);
214            }
215        }
216    }
217
218    private boolean startStop(ViewHolder viewHolder, Message message) {
219        if (message == currentlyPlayingMessage && player != null) {
220            return playPauseCurrent(viewHolder);
221        }
222        if (AudioPlayer.player != null) {
223            stopCurrent();
224        }
225        return play(viewHolder, message, false);
226    }
227
228    private void stopCurrent() {
229        if (AudioPlayer.player.isPlaying()) {
230            AudioPlayer.player.stop();
231        }
232        AudioPlayer.player.release();
233        messageAdapter.flagScreenOff();
234        releaseProximityWakeLock();
235        AudioPlayer.player = null;
236        resetPlayerUi();
237    }
238
239    private void resetPlayerUi() {
240        for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
241            resetPlayerUi(audioPlayer.get());
242        }
243    }
244
245    private void resetPlayerUi(RelativeLayout audioPlayer) {
246        if (audioPlayer == null) {
247            return;
248        }
249        final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
250        final Message message = (Message) audioPlayer.getTag();
251        viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
252        if (message != null) {
253            viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
254        }
255        viewHolder.progress.setProgress(0);
256        viewHolder.progress.setEnabled(false);
257    }
258
259    @Override
260    public void onCompletion(android.media.MediaPlayer mediaPlayer) {
261        synchronized (AudioPlayer.LOCK) {
262            this.stopRefresher(false);
263            if (AudioPlayer.player == mediaPlayer) {
264                AudioPlayer.currentlyPlayingMessage = null;
265                AudioPlayer.player = null;
266            }
267            mediaPlayer.release();
268            messageAdapter.flagScreenOff();
269            releaseProximityWakeLock();
270            resetPlayerUi();
271            sensorManager.unregisterListener(this);
272        }
273    }
274
275    @Override
276    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
277        synchronized (AudioPlayer.LOCK) {
278            final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent();
279            final Message message = (Message) audioPlayer.getTag();
280            if (fromUser && message == AudioPlayer.currentlyPlayingMessage) {
281                float percent = progress / 100f;
282                int duration = AudioPlayer.player.getDuration();
283                int seekTo = Math.round(duration * percent);
284                AudioPlayer.player.seekTo(seekTo);
285            }
286        }
287    }
288
289    @Override
290    public void onStartTrackingTouch(SeekBar seekBar) {
291
292    }
293
294    @Override
295    public void onStopTrackingTouch(SeekBar seekBar) {
296
297    }
298
299    public void stop() {
300        synchronized (AudioPlayer.LOCK) {
301            stopRefresher(false);
302            if (AudioPlayer.player != null) {
303                stopCurrent();
304            }
305            AudioPlayer.currentlyPlayingMessage = null;
306            sensorManager.unregisterListener(this);
307            if (wakeLock != null && wakeLock.isHeld()) {
308                wakeLock.release();
309            }
310            wakeLock = null;
311        }
312    }
313
314    private void stopRefresher(boolean runOnceMore) {
315        this.handler.removeCallbacks(this);
316        if (runOnceMore) {
317            this.handler.post(this);
318        }
319    }
320
321    public void unregisterListener() {
322        if (sensorManager != null) {
323            sensorManager.unregisterListener(this);
324        }
325    }
326
327    @Override
328    public void run() {
329        synchronized (AudioPlayer.LOCK) {
330            if (AudioPlayer.player != null) {
331                boolean renew = false;
332                final int current = player.getCurrentPosition();
333                final int duration = player.getDuration();
334                for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
335                    renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
336                }
337                if (renew && AudioPlayer.player.isPlaying()) {
338                    handler.postDelayed(this, REFRESH_INTERVAL);
339                }
340            }
341        }
342    }
343
344    private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
345        if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
346            return false;
347        }
348        final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
349        if (duration <= 0) {
350            viewHolder.progress.setProgress(100);
351        } else {
352            viewHolder.progress.setProgress(current * 100 / duration);
353        }
354        viewHolder.runtime.setText(String.format("%s / %s", formatTime(current), formatTime(duration)));
355        return true;
356    }
357
358    @Override
359    public void onSensorChanged(SensorEvent event) {
360        if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) {
361            return;
362        }
363        if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) {
364            return;
365        }
366        final int streamType;
367        if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
368            streamType = AudioManager.STREAM_VOICE_CALL;
369        } else {
370            streamType = AudioManager.STREAM_MUSIC;
371        }
372        messageAdapter.setVolumeControl(streamType);
373        double position = AudioPlayer.player.getCurrentPosition();
374        double duration = AudioPlayer.player.getDuration();
375        double progress = position / duration;
376        if (AudioPlayer.player.getAudioStreamType() != streamType) {
377            synchronized (AudioPlayer.LOCK) {
378                AudioPlayer.player.stop();
379                AudioPlayer.player.release();
380                AudioPlayer.player = null;
381                try {
382                    ViewHolder currentViewHolder = getCurrentViewHolder();
383                    if (currentViewHolder != null) {
384                        play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
385                    }
386                } catch (Exception e) {
387                    Log.w(Config.LOGTAG, e);
388                }
389            }
390        }
391    }
392
393    @Override
394    public void onAccuracyChanged(Sensor sensor, int i) {
395    }
396
397    private void acquireProximityWakeLock() {
398        synchronized (AudioPlayer.LOCK) {
399            if (wakeLock != null) {
400                wakeLock.acquire();
401            }
402        }
403    }
404
405    private void releaseProximityWakeLock() {
406        synchronized (AudioPlayer.LOCK) {
407            if (wakeLock != null && wakeLock.isHeld()) {
408                wakeLock.release();
409            }
410        }
411        messageAdapter.setVolumeControl(AudioManager.STREAM_MUSIC);
412    }
413
414    private ViewHolder getCurrentViewHolder() {
415        for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
416            final Message message = (Message) audioPlayer.get().getTag();
417            if (message == currentlyPlayingMessage) {
418                return ViewHolder.get(audioPlayer.get());
419            }
420        }
421        return null;
422    }
423
424    public static class ViewHolder {
425        private TextView runtime;
426        private SeekBar progress;
427        private ImageButton playPause;
428        private boolean darkBackground = false;
429
430        public static ViewHolder get(RelativeLayout audioPlayer) {
431            ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
432            if (viewHolder == null) {
433                viewHolder = new ViewHolder();
434                viewHolder.runtime = audioPlayer.findViewById(R.id.runtime);
435                viewHolder.progress = audioPlayer.findViewById(R.id.progress);
436                viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause);
437                audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
438            }
439            return viewHolder;
440        }
441
442        public void setDarkBackground(boolean darkBackground) {
443            this.darkBackground = darkBackground;
444        }
445    }
446}