View Javadoc

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 			// ideally, the delta will be around 0
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 		// do nothing
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 		// do nothing
308 	}
309 
310 	public void clipboardChanged() {
311 		// do nothing
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 		// is some track selected?
329 		if (currentTrack == null) {
330 			// nope, bail out
331 			seekBar.setProgress(0);
332 			return;
333 		}
334 		if (!currentTrack.getOrigin().seekable) {
335 			// not seekable
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 		// do nothing. this is a low-level player event which is handled by
364 		// PlaylistPlayer
365 	}
366 
367 	public void stopped(String error, boolean errorMissing,
368 			TrackOriginEnum origin) {
369 		// do nothing. this is a low-level player event which is handled by
370 		// PlaylistPlayer - next track will be probably queued shortly.
371 	}
372 
373 	public void lyricsLoaded(TrackMetadataBean track,
374 			final List<LyrdbTrack> lyrics) {
375 		// ignore
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 		// the playlist player component will handle cases when an
390 		// online content is being played.
391 		// try to download cover if missing
392 		if (!offline) {
393 			updateCover();
394 		}
395 	}
396 
397 	public void stateChanged(AppState state) {
398 		// do nothing
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 }