1package eu.siacs.conversations.ui.service;
2
3import android.Manifest;
4import android.content.pm.PackageManager;
5import android.content.res.ColorStateList;
6import android.media.Image;
7import android.media.MediaPlayer;
8import android.os.Build;
9import android.os.Handler;
10import android.support.v4.app.ActivityCompat;
11import android.support.v4.content.ContextCompat;
12import android.view.View;
13import android.widget.ImageButton;
14import android.widget.RelativeLayout;
15import android.widget.SeekBar;
16import android.widget.TextView;
17
18import java.lang.ref.WeakReference;
19import java.util.Locale;
20
21import eu.siacs.conversations.R;
22import eu.siacs.conversations.entities.Message;
23import eu.siacs.conversations.ui.ConversationActivity;
24import eu.siacs.conversations.ui.adapter.MessageAdapter;
25import eu.siacs.conversations.ui.util.PendingItem;
26import eu.siacs.conversations.utils.WeakReferenceSet;
27
28public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable {
29
30 private static final int REFRESH_INTERVAL = 250;
31 private static final Object LOCK = new Object();
32 private static MediaPlayer player = null;
33 private static Message currentlyPlayingMessage = null;
34 private final MessageAdapter messageAdapter;
35 private final WeakReferenceSet<RelativeLayout> audioPlayerLayouts = new WeakReferenceSet<>();
36
37 private final PendingItem<WeakReference<ImageButton>> pendingOnClickView = new PendingItem<>();
38
39 private final Handler handler = new Handler();
40
41 public AudioPlayer(MessageAdapter adapter) {
42 this.messageAdapter = adapter;
43 synchronized (AudioPlayer.LOCK) {
44 if (AudioPlayer.player != null) {
45 AudioPlayer.player.setOnCompletionListener(this);
46 }
47 }
48 }
49
50 private static String formatTime(int ms) {
51 return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59));
52 }
53
54 public void init(RelativeLayout audioPlayer, Message message) {
55 synchronized (AudioPlayer.LOCK) {
56 audioPlayer.setTag(message);
57 if (init(ViewHolder.get(audioPlayer), message)) {
58 this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
59 this.stopRefresher(true);
60 } else {
61 this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer);
62 }
63 }
64 }
65
66 private boolean init(ViewHolder viewHolder, Message message) {
67 if (viewHolder.darkBackground) {
68 viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
69 } else {
70 viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
71 }
72 viewHolder.progress.setOnSeekBarChangeListener(this);
73 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
74 ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.bubble);
75 viewHolder.progress.setThumbTintList(color);
76 viewHolder.progress.setProgressTintList(color);
77 }
78 viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
79 viewHolder.playPause.setOnClickListener(this);
80 if (message == currentlyPlayingMessage) {
81 if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
82 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
83 viewHolder.progress.setEnabled(true);
84 } else {
85 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
86 viewHolder.progress.setEnabled(false);
87 }
88 return true;
89 } else {
90 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
91 viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
92 viewHolder.progress.setProgress(0);
93 viewHolder.progress.setEnabled(false);
94 return false;
95 }
96 }
97
98 @Override
99 public synchronized void onClick(View v) {
100 if (v.getId() == R.id.play_pause) {
101 synchronized (LOCK) {
102 startStop((ImageButton) v);
103 }
104 }
105 }
106
107 private void startStop(ImageButton playPause) {
108 if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
109 pendingOnClickView.push(new WeakReference<>(playPause));
110 ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationActivity.REQUEST_PLAY_PAUSE);
111 return;
112 }
113 final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent();
114 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
115 final Message message = (Message) audioPlayer.getTag();
116 if (startStop(viewHolder, message)) {
117 this.audioPlayerLayouts.clear();
118 this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
119 stopRefresher(true);
120 }
121 }
122
123 private boolean playPauseCurrent(ViewHolder viewHolder) {
124 viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
125 if (player.isPlaying()) {
126 viewHolder.progress.setEnabled(false);
127 player.pause();
128 messageAdapter.flagScreenOff();
129 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
130 } else {
131 viewHolder.progress.setEnabled(true);
132 player.start();
133 messageAdapter.flagScreenOn();
134 this.stopRefresher(true);
135 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
136 }
137 return false;
138 }
139
140 private boolean play(ViewHolder viewHolder, Message message) {
141 AudioPlayer.player = new MediaPlayer();
142 try {
143 AudioPlayer.currentlyPlayingMessage = message;
144 AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
145 AudioPlayer.player.setOnCompletionListener(this);
146 AudioPlayer.player.prepare();
147 AudioPlayer.player.start();
148 messageAdapter.flagScreenOn();
149 viewHolder.progress.setEnabled(true);
150 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
151 return true;
152 } catch (Exception e) {
153 messageAdapter.flagScreenOff();
154 AudioPlayer.currentlyPlayingMessage = null;
155 return false;
156 }
157 }
158
159 public void startStopPending() {
160 WeakReference<ImageButton> reference = pendingOnClickView.pop();
161 if (reference != null) {
162 ImageButton imageButton = reference.get();
163 if (imageButton != null) {
164 startStop(imageButton);
165 }
166 }
167 }
168
169 private boolean startStop(ViewHolder viewHolder, Message message) {
170 if (message == currentlyPlayingMessage && player != null) {
171 return playPauseCurrent(viewHolder);
172 }
173 if (AudioPlayer.player != null) {
174 stopCurrent();
175 }
176 return play(viewHolder, message);
177 }
178
179 private void stopCurrent() {
180 if (AudioPlayer.player.isPlaying()) {
181 AudioPlayer.player.stop();
182 }
183 AudioPlayer.player.release();
184 messageAdapter.flagScreenOff();
185 AudioPlayer.player = null;
186 resetPlayerUi();
187 }
188
189 private void resetPlayerUi() {
190 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
191 resetPlayerUi(audioPlayer.get());
192 }
193 }
194
195 private void resetPlayerUi(RelativeLayout audioPlayer) {
196 if (audioPlayer == null) {
197 return;
198 }
199 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
200 final Message message = (Message) audioPlayer.getTag();
201 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
202 if (message != null) {
203 viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
204 }
205 viewHolder.progress.setProgress(0);
206 viewHolder.progress.setEnabled(false);
207 }
208
209 @Override
210 public void onCompletion(MediaPlayer mediaPlayer) {
211 synchronized (AudioPlayer.LOCK) {
212 this.stopRefresher(false);
213 if (AudioPlayer.player == mediaPlayer) {
214 AudioPlayer.currentlyPlayingMessage = null;
215 AudioPlayer.player = null;
216 }
217 mediaPlayer.release();
218 messageAdapter.flagScreenOff();
219 resetPlayerUi();
220 }
221 }
222
223 @Override
224 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
225 synchronized (AudioPlayer.LOCK) {
226 final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent();
227 final Message message = (Message) audioPlayer.getTag();
228 if (fromUser && message == AudioPlayer.currentlyPlayingMessage) {
229 float percent = progress / 100f;
230 int duration = AudioPlayer.player.getDuration();
231 int seekTo = Math.round(duration * percent);
232 AudioPlayer.player.seekTo(seekTo);
233 }
234 }
235 }
236
237 @Override
238 public void onStartTrackingTouch(SeekBar seekBar) {
239
240 }
241
242 @Override
243 public void onStopTrackingTouch(SeekBar seekBar) {
244
245 }
246
247 public void stop() {
248 synchronized (AudioPlayer.LOCK) {
249 stopRefresher(false);
250 if (AudioPlayer.player != null) {
251 stopCurrent();
252 }
253 AudioPlayer.currentlyPlayingMessage = null;
254 }
255 }
256
257 private void stopRefresher(boolean runOnceMore) {
258 this.handler.removeCallbacks(this);
259 if (runOnceMore) {
260 this.handler.post(this);
261 }
262 }
263
264 @Override
265 public void run() {
266 synchronized (AudioPlayer.LOCK) {
267 if (AudioPlayer.player != null) {
268 boolean renew = false;
269 final int current = player.getCurrentPosition();
270 final int duration = player.getDuration();
271 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
272 renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
273 }
274 if (renew && AudioPlayer.player.isPlaying()) {
275 handler.postDelayed(this, REFRESH_INTERVAL);
276 }
277 }
278 }
279 }
280
281 private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
282 if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
283 return false;
284 }
285 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
286 viewHolder.progress.setProgress(current * 100 / duration);
287 viewHolder.runtime.setText(formatTime(current) + " / " + formatTime(duration));
288 return true;
289 }
290
291 public static class ViewHolder {
292 private TextView runtime;
293 private SeekBar progress;
294 private ImageButton playPause;
295 private boolean darkBackground = false;
296
297 public static ViewHolder get(RelativeLayout audioPlayer) {
298 ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
299 if (viewHolder == null) {
300 viewHolder = new ViewHolder();
301 viewHolder.runtime = (TextView) audioPlayer.findViewById(R.id.runtime);
302 viewHolder.progress = (SeekBar) audioPlayer.findViewById(R.id.progress);
303 viewHolder.playPause = (ImageButton) audioPlayer.findViewById(R.id.play_pause);
304 audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
305 }
306 return viewHolder;
307 }
308
309 public void setDarkBackground(boolean darkBackground) {
310 this.darkBackground = darkBackground;
311 }
312 }
313}