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 if (viewHolder.darkBackground) {
60 viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
61 } else {
62 viewHolder.runtime.setTextAppearance(this.messageAdapter.getContext(), R.style.TextAppearance_Conversations_Caption);
63 }
64 viewHolder.progress.setOnSeekBarChangeListener(this);
65 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
66 ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.bubble);
67 viewHolder.progress.setThumbTintList(color);
68 viewHolder.progress.setProgressTintList(color);
69 }
70 viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
71 viewHolder.playPause.setOnClickListener(this);
72 if (message == currentlyPlayingMessage) {
73 if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) {
74 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
75 viewHolder.progress.setEnabled(true);
76 } else {
77 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
78 viewHolder.progress.setEnabled(false);
79 }
80 return true;
81 } else {
82 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
83 viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
84 viewHolder.progress.setProgress(0);
85 viewHolder.progress.setEnabled(false);
86 return false;
87 }
88 }
89
90 @Override
91 public synchronized void onClick(View v) {
92 if (v.getId() == R.id.play_pause) {
93 synchronized (LOCK) {
94 startStop((ImageButton) v);
95 }
96 }
97 }
98
99 private void startStop(ImageButton playPause) {
100 final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent();
101 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
102 final Message message = (Message) audioPlayer.getTag();
103 if (startStop(viewHolder, message)) {
104 this.audioPlayerLayouts.clear();
105 this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
106 stopRefresher(true);
107 }
108 }
109
110 private boolean playPauseCurrent(ViewHolder viewHolder) {
111 viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f);
112 if (player.isPlaying()) {
113 viewHolder.progress.setEnabled(false);
114 player.pause();
115 messageAdapter.flagScreenOff();
116 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
117 } else {
118 viewHolder.progress.setEnabled(true);
119 player.start();
120 messageAdapter.flagScreenOn();
121 this.stopRefresher(true);
122 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
123 }
124 return false;
125 }
126
127 private boolean play(ViewHolder viewHolder, Message message) {
128 AudioPlayer.player = new MediaPlayer();
129 try {
130 AudioPlayer.currentlyPlayingMessage = message;
131 AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath());
132 AudioPlayer.player.setOnCompletionListener(this);
133 AudioPlayer.player.prepare();
134 AudioPlayer.player.start();
135 messageAdapter.flagScreenOn();
136 viewHolder.progress.setEnabled(true);
137 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp);
138 return true;
139 } catch (Exception e) {
140 messageAdapter.flagScreenOff();
141 AudioPlayer.currentlyPlayingMessage = null;
142 return false;
143 }
144 }
145
146 private boolean startStop(ViewHolder viewHolder, Message message) {
147 if (message == currentlyPlayingMessage && player != null) {
148 return playPauseCurrent(viewHolder);
149 }
150 if (AudioPlayer.player != null) {
151 stopCurrent();
152 }
153 return play(viewHolder, message);
154 }
155
156 private void stopCurrent() {
157 if (AudioPlayer.player.isPlaying()) {
158 AudioPlayer.player.stop();
159 }
160 AudioPlayer.player.release();
161 messageAdapter.flagScreenOff();
162 AudioPlayer.player = null;
163 resetPlayerUi();
164 }
165
166 private void resetPlayerUi() {
167 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
168 resetPlayerUi(audioPlayer.get());
169 }
170 }
171
172 private void resetPlayerUi(RelativeLayout audioPlayer) {
173 if (audioPlayer == null) {
174 return;
175 }
176 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
177 final Message message = (Message) audioPlayer.getTag();
178 viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp);
179 if (message != null) {
180 viewHolder.runtime.setText(formatTime(message.getFileParams().runtime));
181 }
182 viewHolder.progress.setProgress(0);
183 viewHolder.progress.setEnabled(false);
184 }
185
186 @Override
187 public void onCompletion(MediaPlayer mediaPlayer) {
188 synchronized (AudioPlayer.LOCK) {
189 this.stopRefresher(false);
190 if (AudioPlayer.player == mediaPlayer) {
191 AudioPlayer.currentlyPlayingMessage = null;
192 AudioPlayer.player = null;
193 }
194 mediaPlayer.release();
195 messageAdapter.flagScreenOff();
196 resetPlayerUi();
197 }
198 }
199
200 @Override
201 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
202 synchronized (AudioPlayer.LOCK) {
203 final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent();
204 final Message message = (Message) audioPlayer.getTag();
205 if (fromUser && message == AudioPlayer.currentlyPlayingMessage) {
206 float percent = progress / 100f;
207 int duration = AudioPlayer.player.getDuration();
208 int seekTo = Math.round(duration * percent);
209 AudioPlayer.player.seekTo(seekTo);
210 }
211 }
212 }
213
214 @Override
215 public void onStartTrackingTouch(SeekBar seekBar) {
216
217 }
218
219 @Override
220 public void onStopTrackingTouch(SeekBar seekBar) {
221
222 }
223
224 public void stop() {
225 synchronized (AudioPlayer.LOCK) {
226 stopRefresher(false);
227 if (AudioPlayer.player != null) {
228 stopCurrent();
229 }
230 AudioPlayer.currentlyPlayingMessage = null;
231 }
232 }
233
234 private void stopRefresher(boolean runOnceMore) {
235 this.handler.removeCallbacks(this);
236 if (runOnceMore) {
237 this.handler.post(this);
238 }
239 }
240
241 @Override
242 public void run() {
243 synchronized (AudioPlayer.LOCK) {
244 if (AudioPlayer.player != null) {
245 boolean renew = false;
246 final int current = player.getCurrentPosition();
247 final int duration = player.getDuration();
248 for (WeakReference<RelativeLayout> audioPlayer : audioPlayerLayouts) {
249 renew |= refreshAudioPlayer(audioPlayer.get(), current, duration);
250 }
251 if (renew && AudioPlayer.player.isPlaying()) {
252 handler.postDelayed(this, REFRESH_INTERVAL);
253 }
254 }
255 }
256 }
257
258 private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) {
259 if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) {
260 return false;
261 }
262 final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
263 viewHolder.progress.setProgress(current * 100 / duration);
264 viewHolder.runtime.setText(formatTime(current) + " / " + formatTime(duration));
265 return true;
266 }
267
268 public static class ViewHolder {
269 private TextView runtime;
270 private SeekBar progress;
271 private ImageButton playPause;
272 private boolean darkBackground = false;
273
274 public static ViewHolder get(RelativeLayout audioPlayer) {
275 ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
276 if (viewHolder == null) {
277 viewHolder = new ViewHolder();
278 viewHolder.runtime = (TextView) audioPlayer.findViewById(R.id.runtime);
279 viewHolder.progress = (SeekBar) audioPlayer.findViewById(R.id.progress);
280 viewHolder.playPause = (ImageButton) audioPlayer.findViewById(R.id.play_pause);
281 audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder);
282 }
283 return viewHolder;
284 }
285
286 public void setDarkBackground(boolean darkBackground) {
287 this.darkBackground = darkBackground;
288 }
289 }
290}