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.IOException;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.concurrent.Callable;
29 import java.util.concurrent.CopyOnWriteArrayList;
30
31 import org.xml.sax.SAXException;
32
33 import sk.baka.ambient.ActionsEnum;
34 import sk.baka.ambient.AmbientApplication;
35 import sk.baka.ambient.R;
36 import sk.baka.ambient.ZoomEnum;
37 import sk.baka.ambient.collection.TrackMetadataBean;
38 import sk.baka.ambient.commons.Interval;
39 import sk.baka.ambient.commons.MiscUtils;
40 import sk.baka.ambient.stream.shoutcast.Radio;
41 import sk.baka.ambient.stream.shoutcast.ShoutcastUtils;
42 import sk.baka.ambient.views.gesturelist.GesturesListView;
43 import android.view.View;
44 import android.widget.TextView;
45
46 /***
47 * Controls the shoutcast server manager.
48 *
49 * @author Martin Vysny
50 */
51 public final class ShoutcastController extends AbstractListController {
52
53 /***
54 * Creates the shoutcast controller instance.
55 *
56 * @param activity
57 * the activity instance.
58 * @param playlistView
59 * the playlist view
60 */
61 public ShoutcastController(final MainActivity activity,
62 final GesturesListView playlistView) {
63 super(R.id.shoutcastbrowser, R.id.shoutcastList, activity);
64 initButtonBar(R.id.shoutcastButtons, actions);
65
66 listView.dragDropViews.clear();
67 listView.dragDropViews.add(playlistView);
68 }
69
70 @Override
71 public void destroy() {
72 listView.dragDropViews.clear();
73 super.destroy();
74 }
75
76 @Override
77 protected void onAction(ActionsEnum action) {
78 if (action == ActionsEnum.ShoutcastNameTrackSwitch) {
79 isRadioNames = !isRadioNames;
80 if (radioList != null) {
81 sortRadioList();
82 update(null);
83 }
84 }
85 if (action == ActionsEnum.Back) {
86 goBack();
87 }
88 super.onAction(action);
89 }
90
91 /***
92 * The actions to display on the Task switcher.
93 */
94 public static final List<ActionsEnum> actions = Collections
95 .unmodifiableList(Arrays.asList(ActionsEnum.Back,
96 ActionsEnum.ShoutcastNameTrackSwitch));
97
98 /***
99 * If <code>true</code> then a genre list is final being shown. If
100 * <code>false</code> then a radio list is being shown.
101 */
102 private volatile boolean isShowingGenre = true;
103
104 /***
105 * Used only when the radio list is shown. If <code>true</code> then the
106 * radio name is shown. If <code>false</code> then the currently played
107 * track name is shown.
108 */
109 private boolean isRadioNames = true;
110
111 /***
112 * Cached genres list.
113 */
114 private volatile List<String> genres = null;
115
116 /***
117 * Returns list of genres. If the genre list is not yet loaded then the
118 * download starts in the background and an empty list is returned.
119 *
120 * @return the genre list.
121 */
122 private List<String> getGenres() {
123 if (genres != null) {
124 return genres;
125 }
126 app.getBackgroundTasks().schedule(new GenreDownloader(),
127 GenreDownloader.class, true,
128 app.getString(R.string.shoutcastDownloadGenres));
129 return Collections.emptyList();
130 }
131
132 private final class GenreDownloader implements Callable<Void> {
133 public Void call() throws SAXException, IOException {
134 final List<String> genres = ShoutcastUtils.parseGenres();
135 ShoutcastController.this.genres = new CopyOnWriteArrayList<String>(
136 genres);
137 updateAsync();
138 return null;
139 }
140 }
141
142 private void updateAsync() {
143 AmbientApplication.getHandler().post(new Runnable() {
144 public void run() {
145 update(null);
146 }
147 });
148 }
149
150 @Override
151 protected void visibilityChanged(boolean visible) {
152 if (visible) {
153 update(null);
154 }
155 }
156
157 @Override
158 protected void recomputeListItems() {
159
160 final boolean offline = app.isOffline();
161 mainView.findViewById(R.id.shoutcastbrowserControls).setVisibility(
162 offline ? View.GONE : View.VISIBLE);
163 mainView.findViewById(R.id.shoutcastOffline).setVisibility(
164 offline ? View.VISIBLE : View.GONE);
165 if (offline) {
166 return;
167 }
168 listView.getModel().getModel().clear();
169 if (!isVisible()) {
170 return;
171 }
172 if (isShowingGenre) {
173 listView.getModel().getModel().addAll(getGenres());
174 } else {
175 if (radioList != null) {
176 listView.getModel().getModel().addAll(radioList);
177 }
178 }
179 }
180
181 @Override
182 public String getHint(Interval highlight) {
183 return highlight.length + " radio(s)";
184 }
185
186 @Override
187 public boolean isComputeTracksLong(Interval interval) {
188 return !interval.isEmpty();
189 }
190
191 @Override
192 public boolean isComputeTracksOnlineOp(Interval interval) {
193 return !interval.isEmpty();
194 }
195
196 @Override
197 public boolean canComputeItems() {
198 return !isShowingGenre;
199 }
200
201 @Override
202 public boolean canHighlight() {
203 return !isShowingGenre;
204 }
205
206 public void itemActivated(int index, Object model) {
207 if (isShowingGenre) {
208 showGenre((String) model);
209 } else {
210 final Radio r = (Radio) model;
211 app.getBackgroundTasks().schedule(new PlaylistDownloader(r),
212 PlaylistDownloader.class, true,
213 app.getString(R.string.shoutcastDownloadURLs));
214 }
215 }
216
217 private final class PlaylistDownloader implements Callable<Void> {
218 private final Radio r;
219 private volatile boolean gettingPlaylist = true;
220 private volatile List<TrackMetadataBean> playlist = null;
221
222 /***
223 * Creates new instance.
224 *
225 * @param r
226 * the radio.
227 */
228 public PlaylistDownloader(Radio r) {
229 this.r = r;
230 }
231
232 public Void call() {
233 if (gettingPlaylist) {
234 playlist = getRadioStreams(r);
235 playlist = Collections.synchronizedList(playlist);
236 gettingPlaylist = false;
237 AmbientApplication.getHandler()
238 .post(MiscUtils.toRunnable(this));
239 } else {
240 app.getPlaylist().add(app.getPlaylist().size(), playlist);
241 }
242 return null;
243 }
244 }
245
246 /***
247 * Loads the radio list.
248 *
249 * @author Martin Vysny
250 */
251 private class RadioDownloader implements Callable<Void> {
252 private final String genre;
253
254 /***
255 * Creates new instance.
256 *
257 * @param genre
258 * the genre.
259 */
260 public RadioDownloader(final String genre) {
261 this.genre = genre;
262 }
263
264 public Void call() throws SAXException, IOException {
265 final List<Radio> list = ShoutcastUtils.getRadioList(genre);
266 radioList = Collections.synchronizedList(list);
267 sortRadioList();
268 isShowingGenre = false;
269 updateAsync();
270 return null;
271 }
272 }
273
274 /***
275 * The radio list.
276 */
277 private volatile List<Radio> radioList = null;
278
279 @Override
280 public void removeItems(Interval remove) {
281 goBack();
282 }
283
284 private void goBack() {
285 if (isShowingGenre) {
286
287 return;
288 }
289 isShowingGenre = true;
290 radioList = null;
291 update(null);
292 }
293
294 public void update(GesturesListView listView, View itemView, int index,
295 Object model) {
296 final TextView view = (TextView) itemView;
297 if (listView.getHighlight().contains(index)) {
298 view.setBackgroundColor(highlightColor);
299 } else {
300 view.setBackgroundColor(0);
301 }
302 if (model instanceof String) {
303 view.setText((String) model);
304 } else {
305 final Radio r = (Radio) model;
306 view.setText(radioToString(r));
307 }
308 }
309
310 private String radioToString(final Radio r) {
311 if (isRadioNames) {
312 return r.name + " (" + r.genre + ")";
313 } else {
314 return r.currentTrack + " (" + r.genre + ")";
315 }
316 }
317
318 private List<TrackMetadataBean> getRadioStreams(final Radio radio) {
319 try {
320 return radio.getRadioURLs();
321 } catch (final Exception ex) {
322 throw new RuntimeException(ex);
323 }
324 }
325
326 @Override
327 public List<TrackMetadataBean> computeTracks(Interval highlight) {
328 if (isShowingGenre)
329 throw new IllegalStateException();
330 final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>();
331 for (int i = highlight.start; i <= highlight.end; i++) {
332 final Radio r = radioList.get(i);
333 result.addAll(getRadioStreams(r));
334 }
335 return new CopyOnWriteArrayList<TrackMetadataBean>(result);
336 }
337
338 private void sortRadioList() {
339 if (isRadioNames) {
340 ShoutcastUtils.sortByName(radioList);
341 } else {
342 Collections.sort(radioList, new Comparator<Radio>() {
343 public int compare(Radio object1, Radio object2) {
344 return object1.currentTrack
345 .compareToIgnoreCase(object2.currentTrack);
346 }
347 });
348 }
349 }
350
351 /***
352 * Browse given genre.
353 *
354 * @param genre
355 * the genre to browse.
356 */
357 public void showGenre(final String genre) {
358
359 app.getBackgroundTasks().schedule(new RadioDownloader(genre),
360 RadioDownloader.class, true,
361 app.getString(R.string.shoutcastDownloadStations));
362 }
363
364 @Override
365 protected void performZoom(final Map<ZoomEnum, Integer> zoom) {
366 super.performZoom(zoom);
367 initButtonBar(R.id.shoutcastButtons, actions);
368 }
369
370 public String toString(Object model) {
371 return radioToString((Radio) model);
372 }
373 }