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}