1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.activity.main;
19
20 import java.text.ParseException;
21 import java.util.Arrays;
22 import java.util.List;
23
24 import sk.baka.ambient.ActionsEnum;
25 import sk.baka.ambient.AmbientApplication;
26 import sk.baka.ambient.AppState;
27 import sk.baka.ambient.ConfigurationBean;
28 import sk.baka.ambient.IApplicationListener;
29 import sk.baka.ambient.IPlaylistPlayerListener;
30 import sk.baka.ambient.PlaylistPlayer;
31 import sk.baka.ambient.R;
32 import sk.baka.ambient.collection.TrackMetadataBean;
33 import sk.baka.ambient.collection.TrackOriginEnum;
34 import sk.baka.ambient.commons.Interval;
35 import sk.baka.ambient.commons.MiscUtils;
36 import sk.baka.ambient.commons.TagFormatter;
37 import sk.baka.ambient.library.ILibraryListener;
38 import sk.baka.ambient.lrc.lyrdb.LyrdbTrack;
39 import sk.baka.ambient.playerservice.IPlayerListener;
40 import sk.baka.ambient.playerservice.PlayerStateEnum;
41 import sk.baka.ambient.playlist.PlaylistItem;
42 import sk.baka.ambient.playlist.Random;
43 import sk.baka.ambient.playlist.Repeat;
44 import sk.baka.ambient.views.TextScroller;
45 import android.graphics.Bitmap;
46 import android.os.Handler;
47 import android.view.View;
48 import android.widget.ImageView;
49 import android.widget.SeekBar;
50 import android.widget.TextView;
51 import android.widget.SeekBar.OnSeekBarChangeListener;
52
53 /***
54 * Controls the player view located on the {@link MainActivity} activity. This
55 * class is thread unsafe and all methods must be called from the
56 * {@link Handler message loop}.
57 *
58 * @author Martin Vysny
59 */
60 public final class PlayerController extends AbstractController implements
61 IPlaylistPlayerListener, IPlayerListener, IApplicationListener,
62 ILibraryListener, OnSeekBarChangeListener {
63 /***
64 * Handles events.
65 */
66 private final Handler handler = AmbientApplication.getHandler();
67
68 private final List<ActionsEnum> actions;
69
70 /***
71 * Creates the player controller.
72 *
73 * @param app
74 * the application instance
75 */
76 public PlayerController(final MainActivity app) {
77 super(R.id.playerWindow, app);
78 seekbar = (SeekBar) mainView.findViewById(R.id.playerSeekbar);
79 actions = Arrays.asList(ActionsEnum.PlaybackPrevious,
80 ActionsEnum.PlaybackPlay, ActionsEnum.PlaybackNext,
81 ActionsEnum.PlaybackStop, ActionsEnum.RepeatNothing,
82 ActionsEnum.RandomNo, ActionsEnum.QueueTracks,
83 ActionsEnum.QueueClear);
84 initButtonBar(R.id.playerButtons, actions);
85 try {
86 formatter = new TagFormatter(
87 this.app.getStateHandler().getConfig().playerTickerFormat);
88 } catch (ParseException e) {
89 throw new RuntimeException(e);
90 }
91 cycle = true;
92 scroller = (TextScroller) mainView.findViewById(R.id.playerTickerText);
93 update(Interval.EMPTY);
94 seekbar.setOnSeekBarChangeListener(this);
95 }
96
97 private TextScroller scroller;
98
99 private SeekBar seekbar;
100
101 /***
102 * Current position in track, in milliseconds.
103 */
104 private int position;
105
106 /***
107 * Current track, may be <code>null</code> if no track is active.
108 */
109 private TrackMetadataBean track;
110
111 /***
112 * Redraws the position text ({@link R.id#playerTime}) and updates seekbar.
113 */
114 private void redrawPosition() {
115 final StringBuilder builder = new StringBuilder(25);
116 if (track != null) {
117 TrackMetadataBean.appendDisplayableLength(position / 1000, builder,
118 false);
119 if (track.getLength() != 0) {
120 builder.append('/');
121 TrackMetadataBean.appendDisplayableLength(track.getLength(),
122 builder, true);
123 }
124 if (track.getBitrate() != 0) {
125 builder.append(' ');
126 builder.append(track.getBitrate());
127 builder.append("kbps");
128 }
129 if (track.getFrequency() != 0) {
130 builder.append(' ');
131 builder.append(track.getFrequency() / 1000);
132 builder.append("khz");
133 }
134 if (!ignoreProgress && (track.getLength() != 0)) {
135 seekbar.setProgress(position / 1000);
136 }
137 } else {
138 if (!ignoreProgress) {
139 seekbar.setProgress(0);
140 }
141 }
142 final TextView timeText = (TextView) mainView
143 .findViewById(R.id.playerTime);
144 timeText.setText(builder.toString());
145 }
146
147 /***
148 * Reschedules the timer and starts ticking if a non-<code>null</code>
149 * {@link #track} is selected and the playback is started. Disables the
150 * timer otherwise.
151 *
152 * @param playing
153 * if <code>true</code> then the playback is started.
154 */
155 private void reschedulePositionTimer(final boolean playing) {
156 handler.removeCallbacks(incrementSongPosition);
157 if ((track == null) || !playing) {
158 return;
159 }
160 final int millisToSecond = 1000 - (position % 1000);
161 handler.postDelayed(incrementSongPosition, millisToSecond);
162 }
163
164 /***
165 * Increments the song position by a second, updates the display and
166 * auto-schedules itself.
167 */
168 private final Runnable incrementSongPosition = new Runnable() {
169 public void run() {
170 final int newposition = app.getPlaylist().getPosition();
171 final int delta = newposition % 1000;
172 position = newposition;
173
174 int millisToSecond = 1000 - delta;
175 if (millisToSecond < 100) {
176 millisToSecond += 1000;
177 position += 1000;
178 }
179 handler.postDelayed(this, millisToSecond);
180 redrawPosition();
181 }
182 };
183
184 public void playbackStateChanged(PlayerStateEnum state) {
185 reschedulePositionTimer(state == PlayerStateEnum.Playing);
186 if (state == PlayerStateEnum.Playing) {
187 actions.set(1, ActionsEnum.PlaybackPause);
188 initButtonBar(R.id.playerButtons, actions);
189 } else {
190 actions.set(1, ActionsEnum.PlaybackPlay);
191 initButtonBar(R.id.playerButtons, actions);
192 }
193 if (state == PlayerStateEnum.Stopped) {
194 position = 0;
195 redrawPosition();
196 }
197 }
198
199 public void trackChanged(final PlaylistItem item, final boolean playing,
200 final int positionMillis) {
201 track = item == null ? null : item.getTrack();
202 if (item == null) {
203 position = 0;
204 } else {
205 position = positionMillis;
206 }
207 updateTrack();
208 reschedulePositionTimer(playing);
209 }
210
211 /***
212 * Updates ticker, seekbar position and min/max values, cover image.
213 */
214 private void updateTrack() {
215 updateTicker();
216 updateCover();
217 if (track != null) {
218 seekbar.setMax(track.getLength() == 0 ? 600 : track.getLength());
219 } else {
220 seekbar.setMax(600);
221 }
222 redrawPosition();
223 }
224
225 private TagFormatter formatter;
226
227 @Override
228 protected void visibilityChanged(boolean visible) {
229 super.visibilityChanged(visible);
230 if (visible) {
231 scroller.resume();
232 } else {
233 scroller.pause();
234 }
235 }
236
237 /***
238 * Updates the song name ticker.
239 */
240 private void updateTicker() {
241 final String format;
242 if (track != null) {
243 format = formatter.format(track);
244 } else {
245 format = "--";
246 }
247 scroller.setScrolledText(format);
248 }
249
250 public void trackPositionChanged(final int position, final boolean playing) {
251 if (track != null) {
252 this.position = position;
253 } else {
254 this.position = 0;
255 }
256 reschedulePositionTimer(playing);
257 redrawPosition();
258 }
259
260 public void randomChanged(Random random) {
261 actions.set(5, ActionsEnum.getAction(random));
262 initButtonBar(R.id.playerButtons, actions);
263 }
264
265 public void repeatChanged(Repeat repeat) {
266 actions.set(4, ActionsEnum.getAction(repeat));
267 initButtonBar(R.id.playerButtons, actions);
268 }
269
270 @Override
271 public void update(final Interval select) {
272 final PlaylistPlayer player = app.getPlaylist();
273 track = player.getCurrentlyPlayingTrack();
274 final PlayerStateEnum state = player.getPlaybackState();
275 if (state != PlayerStateEnum.Stopped) {
276 position = player.getPosition();
277 } else {
278 position = 0;
279 }
280 playbackStateChanged(state);
281 updateTrack();
282 }
283
284 public void playlistChanged(final Interval target) {
285
286 }
287
288 public void configChanged(ConfigurationBean config) {
289 try {
290 formatter = new TagFormatter(
291 app.getStateHandler().getConfig().playerTickerFormat);
292 } catch (ParseException e) {
293 throw new RuntimeException(e);
294 }
295 updateTicker();
296 }
297
298 public void coverLoaded(TrackMetadataBean track) {
299 if (!MiscUtils.nullEquals(this.track, track)) {
300 return;
301 }
302 updateCover();
303 }
304
305 public void libraryUpdate(boolean updateStarted, boolean interrupted,
306 boolean userNotified) {
307
308 }
309
310 public void clipboardChanged() {
311
312 }
313
314 public void radioNewTrack(String name) {
315 if ((track != null) && (name != null)) {
316 track = TrackMetadataBean.newBuilder().getData(track)
317 .setTitle(name).build(track.getTrackId());
318 updateTicker();
319 }
320 }
321
322 public void onProgressChanged(SeekBar seekBar, int progress,
323 boolean fromTouch) {
324 if (!fromTouch)
325 return;
326 final PlaylistPlayer p = AmbientApplication.getInstance().getPlaylist();
327 final TrackMetadataBean currentTrack = p.getCurrentlyPlayingTrack();
328
329 if (currentTrack == null) {
330
331 seekBar.setProgress(0);
332 return;
333 }
334 if (!currentTrack.getOrigin().seekable) {
335
336 seekBar.setProgress(0);
337 return;
338 }
339 if (p.getPlaybackState() == PlayerStateEnum.Stopped) {
340 p.playWithSeek(progress * 1000);
341 } else {
342 p.seek(progress * 1000);
343 }
344 return;
345 }
346
347 private boolean ignoreProgress = false;
348
349 public void onStartTrackingTouch(SeekBar seekBar) {
350 ignoreProgress = true;
351 }
352
353 public void onStopTrackingTouch(SeekBar seekBar) {
354 ignoreProgress = false;
355 }
356
357 public void buffered(byte percent) {
358 final int progress = seekbar.getMax() * percent / 100;
359 seekbar.setSecondaryProgress(progress);
360 }
361
362 public void started(String file, int duration, int currentPosition) {
363
364
365 }
366
367 public void stopped(String error, boolean errorMissing,
368 TrackOriginEnum origin) {
369
370
371 }
372
373 public void lyricsLoaded(TrackMetadataBean track,
374 final List<LyrdbTrack> lyrics) {
375
376 }
377
378 @Override
379 public void destroy() {
380 reschedulePositionTimer(false);
381 scroller.pause();
382 destroyCover();
383 scroller = null;
384 seekbar = null;
385 super.destroy();
386 }
387
388 public void offline(boolean offline) {
389
390
391
392 if (!offline) {
393 updateCover();
394 }
395 }
396
397 public void stateChanged(AppState state) {
398
399 }
400
401 private Bitmap previousCover = null;
402
403 private void destroyCover() {
404 if (previousCover != null) {
405 final ImageView image = (ImageView) mainView
406 .findViewById(R.id.playlist_item_cover);
407 image.setImageResource(R.drawable.cover);
408 previousCover.recycle();
409 previousCover = null;
410 }
411 }
412
413 private void updateCover() {
414 destroyCover();
415 final ImageView image = (ImageView) mainView
416 .findViewById(R.id.playlist_item_cover);
417 if (track != null) {
418 previousCover = app.getCovers().setCover(track, image);
419 image.setVisibility(View.VISIBLE);
420 } else {
421 image.setVisibility(View.INVISIBLE);
422 }
423 }
424
425 @Override
426 protected void performZoom(boolean zoom) {
427 initButtonBar(R.id.playerButtons, actions);
428 }
429 }