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