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