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}