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}