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