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
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
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 }