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.cb;
20  
21  import java.util.ArrayList;
22  import java.util.Formattable;
23  import java.util.List;
24  import java.util.concurrent.Callable;
25  
26  import sk.baka.ambient.ActionsEnum;
27  import sk.baka.ambient.AmbientApplication;
28  import sk.baka.ambient.I18n;
29  import sk.baka.ambient.R;
30  import sk.baka.ambient.activity.main.AbstractListController;
31  import sk.baka.ambient.collection.CategoryEnum;
32  import sk.baka.ambient.collection.ICollection;
33  import sk.baka.ambient.collection.TrackMetadataBean;
34  import sk.baka.ambient.commons.Interval;
35  import sk.baka.ambient.library.ILibraryListener;
36  import sk.baka.ambient.views.gesturelist.GesturesListView;
37  import android.app.Activity;
38  import android.graphics.Typeface;
39  import android.text.SpannableStringBuilder;
40  import android.text.Spanned;
41  import android.text.style.StyleSpan;
42  import android.view.View;
43  import android.widget.TextView;
44  
45  /***
46   * <p>
47   * Controller which displays a collection browser on given list view and allows
48   * user to navigate it using gestures.
49   * </p>
50   * <p>
51   * The controller listens on two actions:
52   * </p>
53   * <li>
54   * <ul>
55   * {@link ActionsEnum#Back} - go back in the menu hierarchy
56   * </ul>
57   * <ul>
58   * {@link ActionsEnum#CollectionYear} to switch year being displayed
59   * </ul>
60   * </li>
61   * 
62   * @author Martin Vysny
63   */
64  public abstract class AbstractCollectionController extends
65  		AbstractListController implements ILibraryListener {
66  
67  	/***
68  	 * The collection to display the data from.
69  	 */
70  	protected final ICollection collection;
71  
72  	/***
73  	 * the ID of {@link TextView} which will display current category path.
74  	 */
75  	private final int categoryPathId;
76  
77  	/***
78  	 * Creates new controller instance. Subclasses should invoke
79  	 * {@link #updateData()} in the constructor to update the display.
80  	 * 
81  	 * @param mainViewId
82  	 *            the view whose visibility is controlled.
83  	 * @param listViewId
84  	 *            the id of the {@link GesturesListView}
85  	 * @param mainActivity
86  	 *            the main activity instance.
87  	 * @param playlistView
88  	 *            the playlist view - target of drag'n'drop operations.
89  	 * @param collection
90  	 *            the collection to display the data from.
91  	 * @param categoryPathId
92  	 *            the ID of {@link TextView} which will display current category
93  	 *            path.
94  	 */
95  	public AbstractCollectionController(final int mainViewId, int listViewId,
96  			Activity mainActivity, final GesturesListView playlistView,
97  			final ICollection collection, final int categoryPathId) {
98  		super(mainViewId, listViewId, mainActivity);
99  		this.collection = collection;
100 		this.categoryPathId = categoryPathId;
101 		// register the drag'n'drop target.
102 		listView.dragDropViews.clear();
103 		listView.dragDropViews.add(playlistView);
104 		reset();
105 	}
106 
107 	@Override
108 	public void destroy() {
109 		for (final IContentManager man : managerChain) {
110 			man.uninitialize();
111 		}
112 		managerChain.clear();
113 		listView.dragDropViews.clear();
114 		super.destroy();
115 	}
116 
117 	/***
118 	 * Resets the controller and shows the first manager, the
119 	 * {@link GroupingManager}.
120 	 */
121 	protected final void reset() {
122 		for (final IContentManager man : managerChain) {
123 			man.uninitialize();
124 		}
125 		managerChain.clear();
126 		final GroupingManager initialManager = new GroupingManager();
127 		initialManager.initialize(false, collection);
128 		manager = initialManager;
129 		managerChain.add(manager);
130 		updateData();
131 	}
132 
133 	private volatile IContentManager manager;
134 
135 	private final List<IContentManager> managerChain = new ArrayList<IContentManager>();
136 
137 	@Override
138 	public final boolean canHighlight() {
139 		return manager.canReturnTracks();
140 	}
141 
142 	public void itemActivated(final int index, final Object model) {
143 		activateItem(index);
144 	}
145 
146 	/***
147 	 * If <code>true</code> then we cannot fetch new data, change manager etc.
148 	 */
149 	private volatile boolean fetchingData = false;
150 
151 	private void activateItem(int index) {
152 		if (fetchingData) {
153 			return;
154 		}
155 		final IContentManager newManager = manager.itemActivated(index);
156 		if (newManager == null) {
157 			return;
158 		}
159 		fetchData(newManager, 1);
160 	}
161 
162 	/***
163 	 * Fetches new data asynchronously. When the data is ready the listview is
164 	 * updated.
165 	 * 
166 	 * @param newManager
167 	 *            replace current manager with this one.
168 	 * @param direction
169 	 *            if -1 then going back. If 1 then going forward. If 0 then
170 	 *            refreshing current manager.
171 	 */
172 	private void fetchData(final IContentManager newManager, final int direction) {
173 		if (fetchingData) {
174 			throw new IllegalStateException();
175 		}
176 		fetchingData = true;
177 		final Callable<Void> fetcher = new Callable<Void>() {
178 			public Void call() throws Exception {
179 				try {
180 					final boolean changed = newManager.initialize(year,
181 							collection);
182 					if (!changed) {
183 						fetchingData = false;
184 						return null;
185 					}
186 					if (Thread.currentThread().isInterrupted()) {
187 						throw new InterruptedException();
188 					}
189 					final Runnable updater = new Runnable() {
190 						public void run() {
191 							if (manager != newManager) {
192 								manager.uninitialize();
193 								manager = newManager;
194 							}
195 							int prevIndex = -1;
196 							if (direction > 0) {
197 								managerChain.add(manager);
198 							} else if (direction < 0) {
199 								managerChain.remove(managerChain.size() - 1);
200 								prevIndex = manager
201 										.getIndexOfPreviouslyActivatedItem(managerChain
202 												.get(managerChain.size() - 1));
203 								managerChain.set(managerChain.size() - 1,
204 										manager);
205 							}
206 							updateData();
207 							if (prevIndex >= 0) {
208 								listView.setSelection(prevIndex);
209 							}
210 							fetchingData = false;
211 						}
212 					};
213 					AmbientApplication.getHandler().post(updater);
214 				} catch (Exception ex) {
215 					fetchingData = false;
216 					throw ex;
217 				}
218 				return null;
219 			}
220 		};
221 		final String caption = collection.getName() + ": "
222 				+ app.getString(R.string.gettingData);
223 		final boolean accepted = (app.getBackgroundTasks().schedule(fetcher,
224 				getClass(), !collection.isLocal(), caption) != null);
225 		if (!accepted) {
226 			fetchingData = false;
227 			return;
228 		}
229 	}
230 
231 	@Override
232 	public void removeItems(Interval remove) {
233 		goBack();
234 	}
235 
236 	private void goBack() {
237 		if (fetchingData) {
238 			return;
239 		}
240 		final IContentManager newManager = manager.goBack();
241 		if (newManager == null) {
242 			return;
243 		}
244 		fetchData(newManager, -1);
245 	}
246 
247 	/***
248 	 * Updates the data which is currently being shown.
249 	 */
250 	protected final void refetchData() {
251 		fetchData(manager, 0);
252 	}
253 
254 	@Override
255 	protected void onAction(ActionsEnum action) {
256 		if (action == ActionsEnum.Back) {
257 			goBack();
258 			return;
259 		}
260 		if (action == ActionsEnum.CollectionYear) {
261 			if (!fetchingData) {
262 				year = !year;
263 				yearChanged(year);
264 				refetchData();
265 			}
266 		}
267 		super.onAction(action);
268 	}
269 
270 	/***
271 	 * Notifies the subclass that the {@link #year} field was changed. By
272 	 * default does nothing.
273 	 * 
274 	 * @param year
275 	 *            the year value.
276 	 */
277 	protected void yearChanged(boolean year) {
278 	}
279 
280 	/***
281 	 * Returns displayable name of the current category.
282 	 * 
283 	 * @return displayable name.
284 	 */
285 	private CharSequence getPathName() {
286 		final SpannableStringBuilder b = new SpannableStringBuilder();
287 		if (managerChain.size() <= 1) {
288 			b.append(managerChain.get(0).getSelectedItemName());
289 			b.setSpan(new StyleSpan(Typeface.ITALIC), 0, b.length(),
290 					Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
291 			return b;
292 		}
293 		boolean isFirst = true;
294 		for (int i = 1; i < managerChain.size(); i++) {
295 			if (isFirst) {
296 				isFirst = false;
297 			} else {
298 				b.append('/');
299 			}
300 			final String manItem = managerChain.get(i).getSelectedItemName();
301 			b.append(manItem);
302 			final boolean lastMan = i == managerChain.size() - 1;
303 			if (lastMan) {
304 				b.setSpan(new StyleSpan(Typeface.ITALIC), b.length()
305 						- manItem.length(), b.length(),
306 						Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
307 			}
308 		}
309 		return b;
310 	}
311 
312 	/***
313 	 * Show year besides the album name?
314 	 */
315 	private volatile boolean year = false;
316 
317 	@Override
318 	protected final void recomputeListItems() {
319 		final List<Object> model = listView.getModel().getModel();
320 		model.clear();
321 		model.addAll(manager.getDisplayableContent());
322 	}
323 
324 	public void update(GesturesListView listView, View itemView, int index,
325 			Object model) {
326 		final TextView view = (TextView) itemView;
327 		if (listView.getHighlight().contains(index)) {
328 			view.setBackgroundColor(highlightColor);
329 		} else {
330 			view.setBackgroundColor(0);
331 		}
332 		view.setText((String) model);
333 	}
334 
335 	/***
336 	 * Updates data shown in the controller.
337 	 */
338 	protected final void updateData() {
339 		update(Interval.EMPTY);
340 		final TextView text = (TextView) mainActivity
341 				.findViewById(categoryPathId);
342 		text.setText(getPathName());
343 	}
344 
345 	@Override
346 	public final List<TrackMetadataBean> computeTracks(Interval highlight) {
347 		try {
348 			return manager.getTracks(highlight);
349 		} catch (final Exception e) {
350 			throw new RuntimeException(e);
351 		}
352 	}
353 
354 	@Override
355 	public final boolean isComputeTracksLong(Interval interval) {
356 		if (manager instanceof TrackManager) {
357 			return false;
358 		}
359 		return !interval.isEmpty();
360 	}
361 
362 	/***
363 	 * Checks if the controller is currently showing a track list.
364 	 * 
365 	 * @return <code>true</code> if yes, <code>false</code> if it shows a
366 	 *         category list.
367 	 */
368 	protected final boolean isShowingTracks() {
369 		return manager instanceof TrackManager;
370 	}
371 
372 	@Override
373 	public boolean isComputeTracksOnlineOp(Interval interval) {
374 		return !collection.isLocal();
375 	}
376 
377 	@Override
378 	public String getHint(Interval highlight) {
379 		final Formattable f;
380 		if (manager instanceof TrackManager) {
381 			f = I18n.newTracks(highlight.length);
382 		} else if (manager instanceof CategoryManager) {
383 			final CategoryEnum cat = ((CategoryManager) manager).getCurrent();
384 			switch (cat) {
385 			case Album:
386 				f = I18n.newAlbums(highlight.length);
387 				break;
388 			case Artist:
389 				f = I18n.newArtists(highlight.length);
390 				break;
391 			case Genre:
392 				f = I18n.newGenres(highlight.length);
393 				break;
394 			case Title:
395 				f = I18n.newTracks(highlight.length);
396 				break;
397 			default:
398 				throw new IllegalStateException();
399 			}
400 		} else {
401 			throw new IllegalStateException();
402 		}
403 		return I18n.format(f);
404 	}
405 
406 	@Override
407 	public final boolean isReadOnly() {
408 		return true;
409 	}
410 
411 	@Override
412 	public final boolean canComputeItems() {
413 		return !(manager instanceof GroupingManager);
414 	}
415 
416 	public void libraryUpdate(boolean updateStarted, boolean interrupted,
417 			boolean userNotified) {
418 		if (!updateStarted) {
419 			updateData();
420 		}
421 	}
422 
423 	public String toString(Object model) {
424 		// should not be called as strings are shown
425 		throw new Error();
426 	}
427 
428 	/***
429 	 * Returns current content manager. Subclasses must not modify the manager.
430 	 * 
431 	 * @return non-<code>null</code> manager.
432 	 */
433 	protected final IContentManager getCurrentManager() {
434 		return manager;
435 	}
436 }