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