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