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  
19  package sk.baka.ambient.activity.main;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.util.Arrays;
24  import java.util.List;
25  import java.util.Map;
26  
27  import sk.baka.ambient.ActionsEnum;
28  import sk.baka.ambient.AmbientApplication;
29  import sk.baka.ambient.AppState;
30  import sk.baka.ambient.ConfigurationBean;
31  import sk.baka.ambient.IApplicationListener;
32  import sk.baka.ambient.IContentListener;
33  import sk.baka.ambient.IPlaylistPlayerListener;
34  import sk.baka.ambient.R;
35  import sk.baka.ambient.ZoomEnum;
36  import sk.baka.ambient.collection.TrackMetadataBean;
37  import sk.baka.ambient.commons.IOUtils;
38  import sk.baka.ambient.commons.Interval;
39  import sk.baka.ambient.commons.MiscUtils;
40  import sk.baka.ambient.lrc.LRCLyrics;
41  import sk.baka.ambient.lrc.lyrdb.LyrdbTrack;
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.ButtonBar;
47  import sk.baka.ambient.views.gesturelist.GesturesListView;
48  import android.content.Intent;
49  import android.net.Uri;
50  import android.view.View;
51  import android.widget.TextView;
52  
53  /***
54   * Controls the "Context" page.
55   * 
56   * @author Martin Vysny
57   */
58  public final class ContextController extends AbstractController implements
59  		IPlaylistPlayerListener, IContentListener, IApplicationListener {
60  
61  	private final List<ActionsEnum> actions = Arrays.asList(
62  			ActionsEnum.GoOnline, ActionsEnum.ShowWiki, ActionsEnum.ShowLyrics,
63  			ActionsEnum.ShowKaraoke, ActionsEnum.RefreshKaraoke);
64  
65  	/***
66  	 * @param mainActivity
67  	 */
68  	public ContextController(MainActivity mainActivity) {
69  		super(R.id.contextbrowser, mainActivity);
70  		content = (TextView) mainActivity.findViewById(R.id.contextContent);
71  		final boolean offline = app.isOffline();
72  		offline(offline);
73  		final boolean forcedOffline = !app.getStateHandler().getStartupState().online;
74  		updateOfflineButton(forcedOffline);
75  	}
76  
77  	private boolean showingKaraoke = false;
78  
79  	private TextView content;
80  
81  	@Override
82  	public void destroy() {
83  		content = null;
84  		setSelectorVisibility(false);
85  		super.destroy();
86  	}
87  
88  	@Override
89  	protected void onAction(ActionsEnum action) {
90  		if (action == ActionsEnum.ShowKaraoke) {
91  			showingKaraoke = !showingKaraoke;
92  			final ButtonBar bar = (ButtonBar) mainActivity
93  					.findViewById(R.id.contextButtons);
94  			bar.highlight(showingKaraoke ? Interval.fromItem(3) : null, null);
95  			initKaraoke(
96  					app.getPlaylist().getCurrentlyPlayingTrack(),
97  					app.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing);
98  		}
99  		if (action == ActionsEnum.RefreshKaraoke) {
100 			final TrackMetadataBean track = app.getPlaylist()
101 					.getCurrentlyPlayingTrack();
102 			if (track != null) {
103 				app.getKaraoke().deleteLyrics(track);
104 			}
105 			content.setText("");
106 			setStatus(-1);
107 			AmbientApplication.getHandler().removeCallbacks(textSwitcher);
108 			setSelectorVisibility(false);
109 			initKaraoke(
110 					track,
111 					app.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing);
112 		}
113 		if (action == ActionsEnum.ShowLyrics) {
114 			showLyrics();
115 		}
116 		if (action == ActionsEnum.ShowWiki) {
117 			showWiki();
118 		}
119 		if (action == ActionsEnum.GoOffline) {
120 			updateOfflineButton(true);
121 		}
122 		if (action == ActionsEnum.GoOnline) {
123 			updateOfflineButton(false);
124 		}
125 		super.onAction(action);
126 	}
127 
128 	private LRCLyrics lyrics = null;
129 
130 	private void initKaraoke(TrackMetadataBean track, final boolean play) {
131 		if ((track == null) || (!showingKaraoke)) {
132 			content.setText("");
133 			setStatus(-1);
134 			AmbientApplication.getHandler().removeCallbacks(textSwitcher);
135 			return;
136 		}
137 		try {
138 			lyrics = null;
139 			// if the track is local, try to find lyrics locally
140 			if (track.isLocal()) {
141 				final File lrc = new File(IOUtils.stripExt(track.getLocation())
142 						+ ".lrc");
143 				if (lrc.exists()) {
144 					lyrics = LRCLyrics.parse(new FileInputStream(lrc));
145 				}
146 			}
147 			if (lyrics == null) {
148 				lyrics = app.getKaraoke().getLyrics(track);
149 			}
150 		} catch (Exception e) {
151 			app.error(ContextController.class, true,
152 					"Failed to get karaoke lyrics", e);
153 			content.setText("");
154 			setStatus(-1);
155 			return;
156 		}
157 		rescheduleKaraoke(play);
158 	}
159 
160 	public void playbackStateChanged(PlayerStateEnum state) {
161 		rescheduleKaraoke(state == PlayerStateEnum.Playing);
162 	}
163 
164 	public void playlistChanged(Interval target) {
165 		// ignore
166 	}
167 
168 	public void randomChanged(Random random) {
169 		// ignore
170 	}
171 
172 	public void repeatChanged(Repeat repeat) {
173 		// ignore
174 	}
175 
176 	public void trackChanged(PlaylistItem track, boolean play,
177 			final int positionMillis) {
178 		setSelectorVisibility(false);
179 		initKaraoke(track == null ? null : track.getTrack(), play);
180 	}
181 
182 	public void trackPositionChanged(final int position, final boolean playing) {
183 		rescheduleKaraoke(playing);
184 	}
185 
186 	private void rescheduleKaraoke(final boolean play) {
187 		AmbientApplication.getHandler().removeCallbacks(textSwitcher);
188 		if (!play || !showingKaraoke) {
189 			setStatus(-1);
190 			content.setText("");
191 			return;
192 		}
193 		if (lyrics == null) {
194 			final boolean offline = !app.getStateHandler().getStartupState().online;
195 			if (offline) {
196 				setStatus(R.string.inOfflineMode);
197 			} else {
198 				setStatus(-1);
199 			}
200 			return;
201 		}
202 		setStatus(R.string.karaokeForTrack);
203 		AmbientApplication.getHandler().post(textSwitcher);
204 	}
205 
206 	private final KaraokeTextSwitcher textSwitcher = new KaraokeTextSwitcher();
207 
208 	/***
209 	 * Once started, continuously reschedules itself and shows correct lyrics
210 	 * line.
211 	 * 
212 	 * @author Martin Vysny
213 	 */
214 	private final class KaraokeTextSwitcher implements Runnable {
215 
216 		public void run() {
217 			if ((lyrics == null) || (!showingKaraoke)) {
218 				return;
219 			}
220 			final int position = app.getPlaylist().getPosition();
221 			final String lyricsLine = lyrics.getLineToDisplay(position);
222 			final long nextLineTime = lyrics.getNextLineTime(position);
223 			if (nextLineTime < 0)
224 				return;
225 			final long delayFromNow = nextLineTime - position;
226 			AmbientApplication.getHandler().postDelayed(this, delayFromNow);
227 			content.setText(lyricsLine);
228 		}
229 	}
230 
231 	public void coverLoaded(TrackMetadataBean track) {
232 		// ignore
233 	}
234 
235 	private void setSelectorVisibility(final boolean visible) {
236 		mainView.findViewById(R.id.contextContent).setVisibility(
237 				visible ? View.GONE : View.VISIBLE);
238 		mainView.findViewById(R.id.contextLyricsSelector).setVisibility(
239 				visible ? View.VISIBLE : View.GONE);
240 		if (lyricsSelector != null) {
241 			lyricsSelector.destroy();
242 			lyricsSelector = null;
243 		}
244 		if (visible) {
245 			lyricsSelector = new LyricsSelectorController();
246 			lyricsSelector.zoom(getZoom());
247 		} else {
248 			lyricsSelector = null;
249 		}
250 	}
251 
252 	private LyricsSelectorController lyricsSelector = null;
253 
254 	/***
255 	 * Shows an immutable list of strings.
256 	 * 
257 	 * @author Martin Vysny
258 	 */
259 	private class LyricsSelectorController extends AbstractListController {
260 		/***
261 		 * Creates new controller.
262 		 */
263 		public LyricsSelectorController() {
264 			super(R.id.contextLyricsSelector, R.id.contextLyricsSelector,
265 					ContextController.this.mainActivity);
266 		}
267 
268 		private List<LyrdbTrack> lyrics = null;
269 		private TrackMetadataBean track;
270 
271 		/***
272 		 * Sets the content and updates the list view.
273 		 * 
274 		 * @param track
275 		 *            show selector for this track.
276 		 * @param lyrics
277 		 *            the content.
278 		 */
279 		public void setContent(final TrackMetadataBean track,
280 				final List<LyrdbTrack> lyrics) {
281 			this.track = track;
282 			this.lyrics = lyrics;
283 			update(null);
284 		}
285 
286 		@Override
287 		protected void recomputeListItems() {
288 			listView.getModel().getModel().clear();
289 			listView.getModel().getModel().addAll(lyrics);
290 		}
291 
292 		public void itemActivated(int index, Object model) {
293 			// download given karaoke.
294 			final LyrdbTrack lyrics = (LyrdbTrack) model;
295 			app.getKaraoke().setLyricsForTrack(track, lyrics);
296 			setSelectorVisibility(false);
297 			initKaraoke(
298 					track,
299 					app.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing);
300 		}
301 
302 		public void update(GesturesListView listView, View itemView, int index,
303 				Object model) {
304 			final boolean highlighted = listView.getHighlight().contains(index);
305 			final int bgColor;
306 			if (highlighted) {
307 				bgColor = highlightColor;
308 			} else {
309 				bgColor = 0;
310 			}
311 			itemView.setBackgroundColor(bgColor);
312 			final TextView text = (TextView) itemView;
313 			text.setText(((LyrdbTrack) model).displayableString);
314 		}
315 
316 		public String toString(Object model) {
317 			return ((LyrdbTrack) model).displayableString;
318 		}
319 	}
320 
321 	public void lyricsLoaded(TrackMetadataBean track,
322 			final List<LyrdbTrack> lyrics) {
323 		if (!track.equals(app.getPlaylist().getCurrentlyPlayingTrack())) {
324 			return;
325 		}
326 		final boolean isSelecting = (lyrics != null) && !lyrics.isEmpty();
327 		setSelectorVisibility(isSelecting);
328 		if (isSelecting) {
329 			setStatus(R.string.karaokeNotFoundSuggestions);
330 			lyricsSelector.setContent(track, lyrics);
331 		} else if (lyrics != null) {
332 			setStatus(R.string.karaokeNotFound);
333 		} else {
334 			initKaraoke(
335 					track,
336 					app.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing);
337 		}
338 	}
339 
340 	public void clipboardChanged() {
341 		// do nothing
342 	}
343 
344 	public void configChanged(ConfigurationBean config) {
345 		// do nothing
346 	}
347 
348 	public void offline(boolean offline) {
349 		checkButton(R.id.contextButtons, 0, !offline);
350 		initKaraoke(app.getPlaylist().getCurrentlyPlayingTrack(), app
351 				.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing);
352 	}
353 
354 	private void updateOfflineButton(final boolean forcedOffline) {
355 		actions.set(0, forcedOffline ? ActionsEnum.GoOnline
356 				: ActionsEnum.GoOffline);
357 		initButtonBar(R.id.contextButtons, actions);
358 	}
359 
360 	private void setStatus(final int strId) {
361 		final TextView tv = (TextView) mainView
362 				.findViewById(R.id.contextStatusBar);
363 		if (strId == -1) {
364 			tv.setText("");
365 		} else {
366 			tv.setText(strId);
367 		}
368 	}
369 
370 	public void stateChanged(AppState state) {
371 		// do nothing
372 	}
373 
374 	private TrackMetadataBean getCurrent() {
375 		final TrackMetadataBean currentTrack = app.getPlaylist()
376 				.getCurrentlyPlayingTrack();
377 		if (currentTrack == null) {
378 			app.error(MainActivity.class, true, mainActivity
379 					.getString(R.string.no_playing_track), null);
380 		}
381 		return currentTrack;
382 	}
383 
384 	private void showLyrics() {
385 		final TrackMetadataBean currentTrack = getCurrent();
386 		if (currentTrack == null) {
387 			return;
388 		}
389 		if (MiscUtils.isEmptyOrWhitespace(currentTrack.getArtist())
390 				|| MiscUtils.isEmptyOrWhitespace(currentTrack.getTitle())) {
391 			app.error(MainActivity.class, true, mainActivity
392 					.getString(R.string.no_track_meta), null);
393 			return;
394 		}
395 		// strip the track title (remove characters after the "(" char)
396 		String title = currentTrack.getTitle();
397 		final int bracket = title.indexOf('(');
398 		title = bracket < 0 ? title : title.substring(0, bracket).trim();
399 		final String browseURI = "http://letras.terra.com.br/winamp.php?musica="
400 				+ IOUtils.encodeURL(title)
401 				+ "&artista="
402 				+ IOUtils.encodeURL(currentTrack.getArtist());
403 		mainActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri
404 				.parse(browseURI)));
405 	}
406 
407 	private void showWiki() {
408 		final TrackMetadataBean currentTrack = getCurrent();
409 		if (currentTrack == null) {
410 			return;
411 		}
412 		if (MiscUtils.isEmptyOrWhitespace(currentTrack.getArtist())) {
413 			app.error(MainActivity.class, true, mainActivity
414 					.getString(R.string.no_track_meta), null);
415 			return;
416 		}
417 		final String browseURI = "http://en.wikipedia.org/wiki/"
418 				+ IOUtils.encodeURL(currentTrack.getArtist());
419 		mainActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri
420 				.parse(browseURI)));
421 	}
422 
423 	@Override
424 	protected void performZoom(final Map<ZoomEnum, Integer> zoom) {
425 		initButtonBar(R.id.contextButtons, actions);
426 		if (lyricsSelector != null) {
427 			lyricsSelector.zoom(getZoom());
428 		}
429 	}
430 }