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.collection.local;
20
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.EnumMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28
29 import junit.framework.Assert;
30 import sk.baka.ambient.collection.CategoryEnum;
31 import sk.baka.ambient.collection.CategoryItem;
32 import sk.baka.ambient.collection.CollectionException;
33 import sk.baka.ambient.collection.CollectionUtils;
34 import sk.baka.ambient.collection.ICollection;
35 import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
36 import sk.baka.ambient.collection.Statistics;
37 import sk.baka.ambient.collection.TrackMetadataBean;
38 import sk.baka.ambient.collection.TrackOriginEnum;
39 import sk.baka.ambient.playlist.Random;
40 import android.content.ContentResolver;
41 import android.database.Cursor;
42 import android.net.Uri;
43 import android.provider.BaseColumns;
44 import android.provider.MediaStore;
45 import android.provider.MediaStore.MediaColumns;
46 import android.provider.MediaStore.Audio.AudioColumns;
47 import android.provider.MediaStore.Audio.Media;
48
49 /***
50 * Wrapper for Android {@link MediaStore} class.
51 *
52 * @author Martin Vysny
53 */
54 public final class MediaStoreCollection implements ICollection {
55
56 /***
57 * Columns used when retrieving tracks from {@link MediaStore}.
58 */
59 static final String[] TRACK_COLUMNS = new String[] {
60 MediaStore.Audio.AudioColumns.ALBUM,
61 MediaStore.Audio.AudioColumns.ARTIST,
62 MediaStore.Audio.AudioColumns.DURATION,
63 MediaStore.Audio.AudioColumns.TRACK,
64 MediaStore.Audio.AudioColumns.YEAR, MediaStore.MediaColumns.SIZE,
65 MediaStore.MediaColumns.TITLE, BaseColumns._ID };
66 private static final String[] ARTIST_FIELDS = new String[] {
67 BaseColumns._ID, MediaStore.Audio.ArtistColumns.ARTIST,
68 MediaStore.Audio.ArtistColumns.NUMBER_OF_ALBUMS,
69 MediaStore.Audio.ArtistColumns.NUMBER_OF_TRACKS };
70 private static final String[] ALBUM_FIELDS = new String[] {
71 BaseColumns._ID, MediaStore.Audio.AlbumColumns.ALBUM,
72 MediaStore.Audio.AlbumColumns.FIRST_YEAR,
73 MediaStore.Audio.AlbumColumns.NUMBER_OF_SONGS };
74 private final ContentResolver resolver;
75
76 /***
77 * Creates new collection wrapper.
78 *
79 * @param resolver
80 * resolves URI. Must not be <code>null</code>.
81 */
82 public MediaStoreCollection(final ContentResolver resolver) {
83 this.resolver = resolver;
84 }
85
86 public CategoryItem deserializeItem(String serializedItem) {
87 return new CategoryItem(Long.valueOf(serializedItem), null, null,
88 CategoryEnum.Origin, -1, -1);
89 }
90
91 public List<CategoryItem> getCategoryList(CategoryEnum request,
92 CategoryItem context, String substring, boolean sortByYear)
93 throws CollectionException, InterruptedException {
94 List<CategoryItem> result = null;
95 if (context == null) {
96 if (request == CategoryEnum.Album) {
97 result = findAlbums(substring);
98 } else if (request == CategoryEnum.Artist) {
99 result = findArtists(substring);
100 } else if (request == CategoryEnum.Genre) {
101 result = findGenres(substring);
102 }
103 } else if (context.category == CategoryEnum.Genre) {
104 if (request == CategoryEnum.Album) {
105 result = findAlbumsForGenre(substring, (Long) context.id);
106 } else if (request == CategoryEnum.Artist) {
107 result = findArtistsForGenre(substring, (Long) context.id);
108 }
109 } else if (context.category == CategoryEnum.Artist) {
110 if (request == CategoryEnum.Album) {
111 result = findAlbumsForArtist(substring, (Long) context.id);
112 }
113 }
114 if (result == null) {
115 throw new CollectionException("Searching for " + request + " in "
116 + context + " is not supported");
117 }
118 if (sortByYear && request == CategoryEnum.Album) {
119 CollectionUtils.sortByYearName(result);
120 } else {
121 CollectionUtils.sortByName(result);
122 }
123 return result;
124 }
125
126 private List<CategoryItem> findAlbumsForArtist(String substring,
127 final Long id) throws InterruptedException {
128 return findAlbumsForArtist(substring, id,
129 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
130 }
131
132 private List<CategoryItem> findAlbumsForArtist(String substring,
133 Long artistId, Uri artistsUri) throws InterruptedException {
134 final Uri albums = Uri.withAppendedPath(Uri.withAppendedPath(
135 artistsUri, artistId.toString()), "albums");
136 final String selection = substring == null ? null : AudioColumns.ALBUM
137 + " LIKE ?";
138 final String[] selectionArgs = substring == null ? null
139 : new String[] { "%" + substring + "%" };
140 final Cursor c = resolver.query(albums, ALBUM_FIELDS, selection,
141 selectionArgs, null);
142 return readAlbums(c);
143 }
144
145 private List<CategoryItem> findArtistsForGenre(String substring,
146 Long genreId) throws InterruptedException {
147 final List<CategoryItem> result = findArtistsForGenre(substring,
148 genreId, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
149 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
150
151
152
153 return result;
154 }
155
156 private boolean isGenreNonEmpty(final Uri genreUri, final Long genreId) {
157 final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
158 genreId.toString()),
159 MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
160 final Cursor c = resolver.query(members, new String[] { "count(*)" },
161 null, null, null);
162 if (c == null) {
163 return false;
164 }
165 Assert.assertTrue(c.moveToFirst());
166 final long count = c.getLong(0);
167 c.close();
168 return count > 0;
169 }
170
171 private List<CategoryItem> findArtistsForGenre(String substring,
172 Long genreId, Uri genreUri, Uri artistUri)
173 throws InterruptedException {
174 final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
175 genreId.toString()),
176 MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
177 final List<CategoryItem> result = new ArrayList<CategoryItem>();
178 final String selection = substring == null ? null : AudioColumns.ALBUM
179 + " LIKE ?";
180 final String[] selectionArgs = substring == null ? null
181 : new String[] { "%" + substring + "%" };
182 final Set<Long> artists = new HashSet<Long>();
183 final Cursor c = resolver.query(members,
184 new String[] { AudioColumns.ARTIST_ID }, selection,
185 selectionArgs, null);
186 if (c == null || !c.moveToFirst()) {
187 return result;
188 }
189 do {
190 final Long artistId = c.getLong(0);
191 artists.add(artistId);
192 } while (c.moveToNext());
193 c.close();
194 for (final Long artistId : artists) {
195 final Cursor c1 = resolver.query(artistUri, ARTIST_FIELDS,
196 BaseColumns._ID + "=?",
197 new String[] { artistId.toString() }, null);
198 result.addAll(readArtists(c1));
199 }
200 return result;
201 }
202
203 private List<CategoryItem> readArtists(Cursor c)
204 throws InterruptedException {
205 final List<CategoryItem> result = new ArrayList<CategoryItem>();
206 if (c == null || !c.moveToFirst()) {
207 return result;
208 }
209 do {
210 if (Thread.currentThread().isInterrupted()) {
211 throw new InterruptedException();
212 }
213 final CategoryItem.Builder b = new CategoryItem.Builder();
214 b.category = CategoryEnum.Artist;
215 b.id = c.getLong(0);
216 b.name = c.getString(1);
217 b.albums = c.getInt(2);
218 b.songs = c.getInt(3);
219 result.add(b.build());
220 } while (c.moveToNext());
221 c.close();
222 return result;
223 }
224
225 private List<CategoryItem> findAlbumsForGenre(String substring, Long genreId)
226 throws InterruptedException {
227 final List<CategoryItem> result = findAlbumsForGenre(substring,
228 genreId, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
229 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
230
231
232
233 return result;
234 }
235
236 private List<CategoryItem> findAlbumsForGenre(String substring, Long id,
237 Uri genreUri, Uri albumUri) throws InterruptedException {
238 final Uri members = Uri.withAppendedPath(Uri.withAppendedPath(genreUri,
239 id.toString()),
240 MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY);
241 final List<CategoryItem> result = new ArrayList<CategoryItem>();
242 final String selection = substring == null ? null : AudioColumns.ALBUM
243 + " LIKE ?";
244 final String[] selectionArgs = substring == null ? null
245 : new String[] { "%" + substring + "%" };
246 final Set<Long> albums = new HashSet<Long>();
247 final Cursor c = resolver.query(members,
248 new String[] { AudioColumns.ALBUM_ID }, selection,
249 selectionArgs, null);
250 if (c == null || !c.moveToFirst()) {
251 return result;
252 }
253 do {
254 final Long albumId = c.getLong(0);
255 albums.add(albumId);
256 } while (c.moveToNext());
257 c.close();
258 for (final Long albumId : albums) {
259 final Cursor c1 = resolver.query(albumUri, ALBUM_FIELDS,
260 BaseColumns._ID + "=?",
261 new String[] { albumId.toString() }, null);
262 result.addAll(readAlbums(c1));
263 }
264 return result;
265 }
266
267 private List<CategoryItem> findGenres(String substring)
268 throws InterruptedException {
269 final List<CategoryItem> result = findGenres(substring,
270 MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI);
271
272
273 return result;
274 }
275
276 private List<CategoryItem> findArtists(String substring)
277 throws InterruptedException {
278 final List<CategoryItem> result = findArtists(substring,
279 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI);
280
281
282 return result;
283 }
284
285 private List<CategoryItem> findAlbums(String substring)
286 throws InterruptedException {
287 final List<CategoryItem> result = findAlbums(substring,
288 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
289
290
291 return result;
292 }
293
294 public String getName() {
295 return "Android MediaStore";
296 }
297
298 public Statistics getStatistics() {
299
300
301
302
303 final Statistics result = getStatistics(
304 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
305 MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
306 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI);
307 return result;
308 }
309
310 private Statistics getStatistics(final Uri mediaUri, final Uri artistUri,
311 final Uri albumUri) {
312 final Statistics result = new Statistics();
313 Cursor c = resolver.query(mediaUri, new String[] { "count(*)",
314 "sum(" + AudioColumns.DURATION + ")",
315 "sum(" + MediaColumns.SIZE + ")" }, null, null, null);
316 if (c != null) {
317 Assert.assertTrue(c.moveToFirst());
318 result.tracks = c.getInt(0);
319 result.length = c.getInt(1) / 1000;
320 result.fileSize = c.getLong(2);
321 c.close();
322 }
323 c = resolver.query(artistUri, new String[] { "count(*)" }, null, null,
324 null);
325 if (c != null) {
326 Assert.assertTrue(c.moveToFirst());
327 result.artists = c.getInt(0);
328 c.close();
329 }
330 c = resolver.query(albumUri, new String[] { "count(*)" }, null, null,
331 null);
332 if (c != null) {
333 Assert.assertTrue(c.moveToFirst());
334 result.albums = c.getInt(0);
335 c.close();
336 }
337 return result;
338 }
339
340 public List<TrackMetadataBean> getTracks(CategoryItem context)
341 throws CollectionException, InterruptedException {
342 final String selection;
343 final String[] selectionArgs = new String[] { context.id.toString() };
344 if (context.category == CategoryEnum.Album) {
345 selection = AudioColumns.ALBUM_ID + "=?";
346 } else if (context.category == CategoryEnum.Artist) {
347 selection = AudioColumns.ARTIST_ID + "=?";
348 } else {
349 throw new CollectionException("Unsupported getTracks for "
350 + context.category);
351 }
352 final Cursor c = resolver.query(Media.EXTERNAL_CONTENT_URI,
353 TRACK_COLUMNS, selection, selectionArgs, null);
354 final List<TrackMetadataBean> result = readTracks(c,
355 Media.EXTERNAL_CONTENT_URI);
356 return result;
357 }
358
359 /***
360 * Reads tracks from given cursor. The cursor must have columns as specified
361 * in {@link #TRACK_COLUMNS}.
362 *
363 * @param c
364 * the cursor to poll. Closed when the method finishes.
365 * @param contentUri
366 * uses this URI when setting the location of retrieved tracks
367 * @return list of tracks.
368 * @throws InterruptedException
369 */
370 static List<TrackMetadataBean> readTracks(Cursor c, final Uri contentUri)
371 throws InterruptedException {
372 final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>();
373 if (c == null || !c.moveToFirst()) {
374 return result;
375 }
376 do {
377 if (Thread.currentThread().isInterrupted()) {
378 throw new InterruptedException();
379 }
380 result.add(readTrack(c, contentUri));
381 } while (c.moveToNext());
382 c.close();
383 return result;
384 }
385
386 /***
387 * Reads a single track from current cursor position. The cursor must have
388 * columns as specified in {@link #TRACK_COLUMNS}.
389 *
390 * @param c
391 * the cursor to poll. The cursor is not moved.
392 * @param contentUri
393 * uses this URI when setting the location of retrieved tracks
394 * @return the track
395 */
396 static TrackMetadataBean readTrack(final Cursor c, final Uri contentUri) {
397 final TrackMetadataBean.Builder b = new TrackMetadataBean.Builder();
398 b.setAlbum(c.getString(0));
399 b.setArtist(c.getString(1));
400 final Long length = c.getLong(2);
401 b.setLength(length == null ? 0 : (int) (length / 1000));
402 b.setTrackNumber(c.getString(3));
403 b.setYearReleased(c.getString(4));
404 final Long fileSize = c.getLong(5);
405 b.setFileSize(fileSize == null ? 0 : fileSize);
406 b.setTitle(c.getString(6));
407 final Long id = c.getLong(7);
408 b.setLocation(contentUri.toString() + "/" + id);
409 b.setOrigin(TrackOriginEnum.LocalFs);
410 return b.build(id);
411 }
412
413 public boolean isLocal() {
414 return true;
415 }
416
417 public List<TrackMetadataBean> searchTracks(String substring)
418 throws InterruptedException {
419
420
421 final List<TrackMetadataBean> result = searchTracksInUri(
422 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, substring);
423 return result;
424 }
425
426 private List<TrackMetadataBean> searchTracksInUri(final Uri uri,
427 final String substring) throws InterruptedException {
428 final StringBuilder where = new StringBuilder();
429 where.append(MediaStore.Audio.AudioColumns.IS_MUSIC);
430 where.append(" != 0");
431 final String[] whereArgs;
432 if (substring != null) {
433 where.append(" and (");
434 where.append(MediaStore.Audio.AudioColumns.ALBUM);
435 where.append(" LIKE ? or ");
436 where.append(MediaStore.Audio.AudioColumns.ARTIST);
437 where.append(" LIKE ? or ");
438 where.append(MediaStore.MediaColumns.TITLE);
439 where.append(" LIKE ?)");
440 final String whereArg = "%" + substring + "%";
441 whereArgs = new String[] { whereArg, whereArg, whereArg };
442 } else {
443 whereArgs = null;
444 }
445 final Cursor c = resolver.query(uri, TRACK_COLUMNS, where.toString(),
446 whereArgs, null);
447 return readTracks(c, uri);
448 }
449
450 public String serializeItem(CategoryItem item) {
451 return item.id.toString();
452 }
453
454 private List<CategoryItem> findAlbums(final String substring,
455 final Uri contentUri) throws InterruptedException {
456 final String selection = substring == null ? null
457 : MediaStore.Audio.AlbumColumns.ALBUM + " LIKE ?";
458 final String[] selectionArgs = substring == null ? null
459 : new String[] { "%" + substring + "%" };
460 final Cursor c = resolver.query(contentUri, ALBUM_FIELDS, selection,
461 selectionArgs, null);
462 return readAlbums(c);
463 }
464
465 private List<CategoryItem> readAlbums(final Cursor c)
466 throws InterruptedException {
467 final List<CategoryItem> result = new ArrayList<CategoryItem>();
468 if (c == null || !c.moveToFirst()) {
469 return result;
470 }
471 do {
472 if (Thread.currentThread().isInterrupted()) {
473 throw new InterruptedException();
474 }
475 final CategoryItem.Builder b = new CategoryItem.Builder();
476 b.albums = 1;
477 b.category = CategoryEnum.Album;
478 b.id = c.getLong(0);
479 b.name = c.getString(1);
480 b.year = c.getString(2);
481 b.songs = c.getInt(3);
482 result.add(b.build());
483 } while (c.moveToNext());
484 c.close();
485 return result;
486 }
487
488 private List<CategoryItem> findArtists(final String substring,
489 final Uri contentUri) throws InterruptedException {
490 final String selection = substring == null ? null
491 : MediaStore.Audio.ArtistColumns.ARTIST + " LIKE ?";
492 final String[] selectionArgs = substring == null ? null
493 : new String[] { "%" + substring + "%" };
494 final Cursor c = resolver.query(contentUri, ARTIST_FIELDS, selection,
495 selectionArgs, null);
496 return readArtists(c);
497 }
498
499 private List<CategoryItem> findGenres(final String substring,
500 final Uri contentUri) throws InterruptedException {
501 final List<CategoryItem> result = new ArrayList<CategoryItem>();
502 final String selection = substring == null ? null
503 : MediaStore.Audio.GenresColumns.NAME + " LIKE ?";
504 final String[] selectionArgs = substring == null ? null
505 : new String[] { "%" + substring + "%" };
506 final Cursor c = resolver.query(contentUri, new String[] {
507 BaseColumns._ID, MediaStore.Audio.GenresColumns.NAME },
508 selection, selectionArgs, null);
509 if (c == null || !c.moveToFirst()) {
510 return result;
511 }
512 do {
513 if (Thread.currentThread().isInterrupted()) {
514 throw new InterruptedException();
515 }
516 final long genreId = c.getLong(0);
517 if (!isGenreNonEmpty(contentUri, genreId)) {
518 continue;
519 }
520 final CategoryItem.Builder b = new CategoryItem.Builder();
521 b.category = CategoryEnum.Genre;
522 b.id = genreId;
523 b.name = c.getString(1);
524 b.albums = -1;
525 b.songs = -1;
526 result.add(b.build());
527 } while (c.moveToNext());
528 return result;
529 }
530
531 public IDynamicPlaylistTrackProvider newTrackProvider(final Random random) {
532 return new MediaStoreTrackProvider(random);
533 }
534
535 public List<TrackMetadataBean> findTracks(Map<CategoryEnum, String> criteria)
536 throws CollectionException, InterruptedException {
537 return findTracksInUri(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
538 criteria);
539 }
540
541 private final static Map<CategoryEnum, String> COLUMNS = new EnumMap<CategoryEnum, String>(
542 CategoryEnum.class);
543 static {
544 COLUMNS.put(CategoryEnum.Album, AudioColumns.ALBUM);
545 COLUMNS.put(CategoryEnum.Artist, AudioColumns.ARTIST);
546 COLUMNS.put(CategoryEnum.Title, MediaColumns.TITLE);
547 COLUMNS.put(CategoryEnum.YearReleased, AudioColumns.YEAR);
548 }
549
550 private List<TrackMetadataBean> findTracksInUri(final Uri uri,
551 final Map<CategoryEnum, String> criteria)
552 throws InterruptedException {
553 final StringBuilder where = new StringBuilder();
554 where.append("(");
555 where.append(MediaStore.Audio.AudioColumns.IS_MUSIC);
556 where.append(" != 0)");
557 final List<String> whereArgs = new ArrayList<String>();
558 for (final Map.Entry<CategoryEnum, String> entry : criteria.entrySet()) {
559 final String column = COLUMNS.get(entry.getKey());
560 if (column == null) {
561 continue;
562 }
563 where.append(" and (");
564 where.append(column);
565 where.append(" LIKE ?)");
566 whereArgs.add("%" + entry.getValue() + "%");
567 }
568 final Cursor c = resolver.query(uri, TRACK_COLUMNS, where.toString(),
569 whereArgs.toArray(new String[0]), null);
570 return readTracks(c, uri);
571 }
572
573 public Map<String, String> fixLocations(Collection<String> locations) {
574 throw new UnsupportedOperationException();
575 }
576
577 public boolean supportsLocationFix() {
578 return false;
579 }
580 }