package org.music.player; import android.content.ContentResolver; import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import java.io.File; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; import junit.framework.Assert; /** * Provides some static Song/MediaStore-related utility functions. */ public class MediaUtils { /** * A special invalid media type. */ public static final int TYPE_INVALID = -1; /** * Type indicating an id represents an artist. */ public static final int TYPE_ARTIST = 0; /** * Type indicating an id represents an album. */ public static final int TYPE_ALBUM = 1; /** * Type indicating an id represents a song. */ public static final int TYPE_SONG = 2; /** * Type indicating an id represents a playlist. */ public static final int TYPE_PLAYLIST = 3; /** * Type indicating ids represent genres. */ public static final int TYPE_GENRE = 4; /** * Special type for files and folders. Most methods do not accept this type * since files have no MediaStore id and require special handling. */ public static final int TYPE_FILE = 5; /** * The number of different valid media types. */ public static final int TYPE_COUNT = 6; /** * The default sort order for media queries. First artist, then album, then * track number. */ public static final String DEFAULT_SORT = "artist_key,album_key,track"; /** * Cached random instance. */ private static Random sRandom; /** * Shuffled list of all ids in the library. */ private static long[] sAllSongs; private static int sAllSongsIdx; /** * Query this many songs at a time from sAllSongs. */ private static final int RANDOM_POPULATE_SIZE = 20; private static final Song[] sRandomCache = new Song[RANDOM_POPULATE_SIZE]; private static int sRandomCacheIdx; private static int sRandomCacheEnd; /** * Total number of songs in the music library, or -1 for uninitialized. */ private static int sSongCount = -1; /** * Returns a cached random instanced, creating it if necessary. */ public static Random getRandom() { if (sRandom == null) sRandom = new Random(); return sRandom; } /** * Builds a query that will return all the songs represented by the given * parameters. * * @param type MediaUtils.TYPE_ARTIST, TYPE_ALBUM, or TYPE_SONG. * @param id The MediaStore id of the song, artist, or album. * @param projection The columns to query. * @param select An extra selection to pass to the query, or null. * @return The initialized query. */ public static QueryTask buildMediaQuery(int type, long id, String[] projection, String select) { Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; StringBuilder selection = new StringBuilder(); switch (type) { case TYPE_SONG: selection.append(MediaStore.Audio.Media._ID); break; case TYPE_ARTIST: selection.append(MediaStore.Audio.Media.ARTIST_ID); break; case TYPE_ALBUM: selection.append(MediaStore.Audio.Media.ALBUM_ID); break; default: throw new IllegalArgumentException("Invalid type specified: " + type); } selection.append('='); selection.append(id); selection.append(" AND is_music!=0"); if (select != null) { selection.append(" AND "); selection.append(select); } QueryTask result = new QueryTask(media, projection, selection.toString(), null, DEFAULT_SORT); result.type = type; return result; } /** * Builds a query that will return all the songs in the playlist with the * given id. * * @param id The id of the playlist in MediaStore.Audio.Playlists. * @param projection The columns to query. * @param selection The selection to pass to the query, or null. * @return The initialized query. */ public static QueryTask buildPlaylistQuery(long id, String[] projection, String selection) { Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", id); String sort = MediaStore.Audio.Playlists.Members.PLAY_ORDER; QueryTask result = new QueryTask(uri, projection, selection, null, sort); result.type = TYPE_PLAYLIST; return result; } /** * Builds a query that will return all the songs in the genre with the * given id. * * @param id The id of the genre in MediaStore.Audio.Genres. * @param projection The columns to query. * @param selection The selection to pass to the query, or null. * @param selectionArgs The arguments to substitute into the selection. * @param sort The sort order. */ public static QueryTask buildGenreQuery(long id, String[] projection, String selection, String[] selectionArgs, String sort) { Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", id); QueryTask result = new QueryTask(uri, projection, selection, selectionArgs, sort); result.type = TYPE_GENRE; return result; } /** * Builds a query with the given information. * * @param type Type the id represents. Must be one of the Song.TYPE_* * constants. * @param id The id of the element in the MediaStore content provider for * the given type. * @param selection An extra selection to be passed to the query. May be * null. Must not be used with type == TYPE_SONG or type == TYPE_PLAYLIST */ public static QueryTask buildQuery(int type, long id, String[] projection, String selection) { switch (type) { case TYPE_ARTIST: case TYPE_ALBUM: case TYPE_SONG: return buildMediaQuery(type, id, projection, selection); case TYPE_PLAYLIST: return buildPlaylistQuery(id, projection, selection); case TYPE_GENRE: return buildGenreQuery(id, projection, selection, null, MediaStore.Audio.Genres.Members.TITLE_KEY); default: throw new IllegalArgumentException("Specified type not valid: " + type); } } /** * Query the MediaStore to determine the id of the genre the song belongs * to. * * @param resolver A ContentResolver to use. * @param id The id of the song to query the genre for. */ public static long queryGenreForSong(ContentResolver resolver, long id) { String[] projection = { "_id" }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { Uri uri = CompatHoneycomb.getContentUriForAudioId((int)id); Cursor cursor = resolver.query(uri, projection, null, null, null); if (cursor != null) { if (cursor.moveToNext()) return cursor.getLong(0); cursor.close(); } } else { Uri uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; Cursor cursor = resolver.query(uri, projection, null, null, null); if (cursor != null) { String selection = "_id=" + id; while (cursor.moveToNext()) { // check if the given song belongs to this genre long genreId = cursor.getLong(0); Uri genreUri = MediaStore.Audio.Genres.Members.getContentUri("external", genreId); Cursor c = resolver.query(genreUri, projection, selection, null, null); if (c != null) { if (c.getCount() == 1) return genreId; c.close(); } } cursor.close(); } } return 0; } /** * Shuffle an array using Fisher-Yates algorithm. * * @param list The array. It will be shuffled in place. */ public static void shuffle(long[] list) { Random random = getRandom(); for (int i = list.length; --i != -1; ) { int j = random.nextInt(i + 1); long tmp = list[j]; list[j] = list[i]; list[i] = tmp; } } /** * Shuffle an array using Fisher-Yates algorithm. * * @param list The array. It will be shuffled in place. * @param end Only elements before this index will be shuffled. */ public static void shuffle(Song[] list, int end) { Assert.assertTrue(end <= list.length && end >= 0); Random random = getRandom(); for (int i = end; --i != -1; ) { int j = random.nextInt(i + 1); Song tmp = list[j]; list[j] = list[i]; list[i] = tmp; } } /** * Shuffle a Song list using Fisher-Yates algorithm. * * @param albumShuffle If true, preserve the order of tracks inside albums. */ public static void shuffle(List<Song> list, boolean albumShuffle) { int size = list.size(); if (size < 2) return; Random random = getRandom(); if (albumShuffle) { Song[] songs = list.toArray(new Song[size]); Song[] temp = new Song[size]; // Make sure the albums are in order Arrays.sort(songs); // This is Fisher-Yates algorithm, but it swaps albums instead of // single elements. for (int i = size; --i != -1; ) { Song songI = songs[i]; if (i > 0 && songs[i - 1].albumId == songI.albumId) // This index is not the start of an album. Skip it. continue; int j = random.nextInt(i + 1); while (j > 0 && songs[j - 1].albumId == songs[j].albumId) // This index is not the start of an album. Find the start. j -= 1; int lowerStart = Math.min(i, j); int upperStart = Math.max(i, j); if (lowerStart == upperStart) // Swap with ourself. That was easy! continue; long lowerAlbum = songs[lowerStart].albumId; int lowerEnd = lowerStart; while (lowerEnd + 1 < size && songs[lowerEnd + 1].albumId == lowerAlbum) lowerEnd += 1; long upperAlbum = songs[upperStart].albumId; int upperEnd = upperStart; while (upperEnd + 1 < size && songs[upperEnd + 1].albumId == upperAlbum) upperEnd += 1; int lowerSize = lowerEnd - lowerStart + 1; int upperSize = upperEnd - upperStart + 1; if (lowerSize == 1 && upperSize == 1) { // Easy, single element swap Song tempSong = songs[lowerStart]; songs[lowerStart] = songs[upperStart]; songs[upperStart] = tempSong; } else { // Slow multi-element swap. Copy to a new array in the // swapped order. System.arraycopy(songs, 0, temp, 0, lowerStart); // copy elements before lower System.arraycopy(songs, upperStart, temp, lowerStart, upperSize); // copy upper elements to lower spot System.arraycopy(songs, lowerEnd + 1, temp, lowerStart + upperSize, upperStart - lowerEnd - 1); // copy elements between upper and lower System.arraycopy(songs, lowerStart, temp, lowerStart + upperEnd - lowerEnd, lowerSize); // copy lower elements to upper spot System.arraycopy(songs, upperEnd + 1, temp, upperEnd + 1, size - upperEnd - 1); // copy elements remaining elements after upper // New array is finished. Use the old array as temp for the // next iteration. Song[] tempTemp = songs; songs = temp; temp = tempTemp; } } list.clear(); list.addAll(Arrays.asList(songs)); } else { Collections.shuffle(list, random); } } /** * Determine if any songs are available from the library. * * @param resolver A ContentResolver to use. * @return True if it's possible to retrieve any songs, false otherwise. For * example, false could be returned if there are no songs in the library. */ public static boolean isSongAvailable(ContentResolver resolver) { if (sSongCount == -1) { Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String selection = MediaStore.Audio.Media.IS_MUSIC + "!=0"; Cursor cursor = resolver.query(media, new String[]{"count(_id)"}, selection, null, null); if (cursor == null) { sSongCount = 0; } else { cursor.moveToFirst(); sSongCount = cursor.getInt(0); cursor.close(); } } return sSongCount != 0; } /** * Returns a shuffled array contaning the ids of all the songs on the * device's library. * * @param resolver A ContentResolver to use. */ public static long[] queryAllSongs(ContentResolver resolver) { sAllSongsIdx = 0; sRandomCacheEnd = -1; Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String selection = MediaStore.Audio.Media.IS_MUSIC + "!=0"; Cursor cursor = resolver.query(media, Song.EMPTY_PROJECTION, selection, null, null); if (cursor == null || cursor.getCount() == 0) { sSongCount = 0; return null; } int count = cursor.getCount(); long[] ids = new long[count]; for (int i = 0; i != count; ++i) { if (!cursor.moveToNext()) return null; ids[i] = cursor.getLong(0); } sSongCount = count; cursor.close(); shuffle(ids); return ids; } public static void onMediaChange() { sSongCount = -1; sAllSongs = null; } /** * Returns a song randomly selected from all the songs in the Android * MediaStore. * * @param resolver A ContentResolver to use. */ public static Song randomSong(ContentResolver resolver) { long[] songs = sAllSongs; if (songs == null) { songs = queryAllSongs(resolver); if (songs == null) return null; sAllSongs = songs; } else if (sAllSongsIdx == sAllSongs.length) { sAllSongsIdx = 0; sRandomCacheEnd = -1; shuffle(sAllSongs); } if (sAllSongsIdx >= sRandomCacheEnd) { Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; StringBuilder selection = new StringBuilder("_ID IN ("); boolean first = true; int end = Math.min(sAllSongsIdx + RANDOM_POPULATE_SIZE, sAllSongs.length); for (int i = sAllSongsIdx; i != end; ++i) { if (!first) selection.append(','); first = false; selection.append(sAllSongs[i]); } selection.append(')'); Cursor cursor = resolver.query(media, Song.FILLED_PROJECTION, selection.toString(), null, null); if (cursor == null) { sAllSongs = null; return null; } int count = cursor.getCount(); if (count > 0) { Assert.assertTrue(count <= RANDOM_POPULATE_SIZE); for (int i = 0; i != count; ++i) { cursor.moveToNext(); Song newSong = new Song(-1); newSong.populate(cursor); newSong.flags |= Song.FLAG_RANDOM; sRandomCache[i] = newSong; } } cursor.close(); // The query will return sorted results; undo that shuffle(sRandomCache, count); sRandomCacheIdx = 0; sRandomCacheEnd = sAllSongsIdx + count; } Song result = sRandomCache[sRandomCacheIdx]; ++sRandomCacheIdx; ++sAllSongsIdx; return result; } /** * Delete the given file or directory recursively. * * @return True if successful; false otherwise. */ public static boolean deleteFile(File file) { File[] children = file.listFiles(); if (children != null) { for (File child : children) { deleteFile(child); } } return file.delete(); } /** * Build a query that will contain all the media under the given path. * * @param path The path, e.g. /mnt/sdcard/music/ * @param projection The columns to query * @return The initialized query. */ public static QueryTask buildFileQuery(String path, String[] projection) { // It would be better to use selectionArgs to pass path here, but there // doesn't appear to be any way to pass the * when using it. StringBuilder selection = new StringBuilder(); selection.append("_data GLOB "); DatabaseUtils.appendEscapedSQLString(selection, path); // delete the quotation mark added by the escape method selection.deleteCharAt(selection.length() - 1); selection.append("*' AND is_music!=0"); Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; QueryTask result = new QueryTask(media, projection, selection.toString(), null, DEFAULT_SORT); result.type = TYPE_FILE; return result; } }