package com.kure.musicplayer.model; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; /** * Global interface to all the songs this application can see. * * Tasks: * - Scans for songs on the device * (both internal and external memories) * - Has query functions to songs and their attributes. * * Thanks: * * - Showing me how to get a music's full PATH: * http://stackoverflow.com/a/21333187 * * - Teaching me the queries to get Playlists * and their songs: * http://stackoverflow.com/q/11292125 */ public class SongList { /** * Big list with all the Songs found. */ public ArrayList<Song> songs = new ArrayList<Song>(); /** * Big list with all the Playlists found. */ public ArrayList<Playlist> playlists = new ArrayList<Playlist>(); /** * Maps song's genre IDs to song's genre names. * @note It's only available after calling `scanSongs`. */ private HashMap<String, String> genreIdToGenreNameMap; /** * Maps song's IDs to song genre IDs. * @note It's only available after calling `scanSongs`. */ private HashMap<String, String> songIdToGenreIdMap; /** * Flag that tells if successfully scanned all songs. */ private boolean scannedSongs; /** * Flag that tells if we're scanning songs right now. */ private boolean scanningSongs; /** * Tells if we've successfully scanned all songs on * the device. * * This will return `false` both while we're scanning * for songs and if some error happened while scanning. */ public boolean isInitialized() { return scannedSongs; } /** * Tells if we're currently scanning songs on the device. */ public boolean isScanning() { return scanningSongs; } /** * Scans the device for songs. * * This function takes a lot of time to execute and * blocks the program UI. * So you should call it on a separate thread and * query `isInitialized` when needed. * * Inside it, we make a lot of queries to the system's * databases - getting songs, genres and playlists. * * @note If you call this function twice, it rescans * the songs, refreshing internal lists. * It doesn't add up songs. * * @param c The current Activity's Context. * @param fromWhere Where should we scan for songs. * * Accepted values to `fromWhere` are: * - "internal" To scan for songs on the phone's memory. * - "external" To scan for songs on the SD card. * - "both" To scan for songs anywhere. */ public void scanSongs(Context c, String fromWhere) { // This is a rather complex function that interacts with // the underlying Android database. // Grab some coffee and stick to the comments. // Not implemented yet. if (fromWhere == "both") throw new RuntimeException("Can't scan from both locations - not implemented"); // Checking for flags so we don't get called twice // Java doesn't allow local static variables. if (scanningSongs) return; scanningSongs = true; // The URIs that tells where we should scan for files. // There are separate URIs for music, genres and playlists. Go figure... // // Remember - internal is the phone memory, external is for the SD card. Uri musicUri = ((fromWhere == "internal") ? android.provider.MediaStore.Audio.Media.INTERNAL_CONTENT_URI: android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI); Uri genreUri = ((fromWhere == "internal") ? android.provider.MediaStore.Audio.Genres.INTERNAL_CONTENT_URI: android.provider.MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI); Uri playlistUri = ((fromWhere == "internal") ? android.provider.MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI: android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI); // Gives us access to query for files on the system. ContentResolver resolver = c.getContentResolver(); // We use this thing to iterate through the results // of a SQLite database query. Cursor cursor; // OK, this is where we start. // // First, before even touching the songs, we'll save all the // music genres (like "Rock", "Jazz" and such). // That's because Android doesn't allow getting a song genre // from the song file itself. // // To get the genres, we make queries to the system's SQLite // database. It involves genre IDs, music IDs and such. // // We're creating two maps: // // 1. Genre ID -> Genre Names // 2. Song ID -> Genre ID // // This way, we have a connection from a Song ID to a Genre Name. // // Then we finally get the songs! // We make queries to the database, getting all possible song // metadata - like artist, album and such. // These are the columns from the system databases. // They're the information I want to get from songs. String GENRE_ID = MediaStore.Audio.Genres._ID; String GENRE_NAME = MediaStore.Audio.Genres.NAME; String SONG_ID = android.provider.MediaStore.Audio.Media._ID; String SONG_TITLE = android.provider.MediaStore.Audio.Media.TITLE; String SONG_ARTIST = android.provider.MediaStore.Audio.Media.ARTIST; String SONG_ALBUM = android.provider.MediaStore.Audio.Media.ALBUM; String SONG_YEAR = android.provider.MediaStore.Audio.Media.YEAR; String SONG_TRACK_NO = android.provider.MediaStore.Audio.Media.TRACK; String SONG_FILEPATH = android.provider.MediaStore.Audio.Media.DATA; String SONG_DURATION = android.provider.MediaStore.Audio.Media.DURATION; // Creating the map "Genre IDs" -> "Genre Names" genreIdToGenreNameMap = new HashMap<String, String>(); // This is what we'll ask of the genres String[] genreColumns = { GENRE_ID, GENRE_NAME }; // Actually querying the genres database cursor = resolver.query(genreUri, genreColumns, null, null, null); // Iterating through the results and filling the map. for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) genreIdToGenreNameMap.put(cursor.getString(0), cursor.getString(1)); cursor.close(); // Map from Songs IDs to Genre IDs songIdToGenreIdMap = new HashMap<String, String>(); // UPDATE URI HERE if (fromWhere == "both") throw new RuntimeException("Can't scan from both locations - not implemented"); // For each genre, we'll query the databases to get // all songs's IDs that have it as a genre. for (String genreID : genreIdToGenreNameMap.keySet()) { Uri uri = MediaStore.Audio.Genres.Members.getContentUri(fromWhere, Long.parseLong(genreID)); cursor = resolver.query(uri, new String[] { SONG_ID }, null, null, null); // Iterating through the results, populating the map for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long currentSongID = cursor.getLong(cursor.getColumnIndex(SONG_ID)); songIdToGenreIdMap.put(Long.toString(currentSongID), genreID); } cursor.close(); } // Finished getting the Genres. // Let's go get dem songzz. // Columns I'll retrieve from the song table String[] columns = { SONG_ID, SONG_TITLE, SONG_ARTIST, SONG_ALBUM, SONG_YEAR, SONG_TRACK_NO, SONG_FILEPATH, SONG_DURATION }; // Thing that limits results to only show music files. // // It's a SQL "WHERE" clause - it becomes `WHERE IS_MUSIC=1`. // // (note: using `IS_MUSIC!=0` takes a huge load of time) final String musicsOnly = MediaStore.Audio.Media.IS_MUSIC + "=1"; // Actually querying the system cursor = resolver.query(musicUri, columns, musicsOnly, null, null); if (cursor != null && cursor.moveToFirst()) { // NOTE: I tried to use MediaMetadataRetriever, but it was too slow. // Even with 10 songs, it took like 13 seconds, // No way I'm releasing it this way - I have like 4.260 songs! do { // Creating a song from the values on the row Song song = new Song(cursor.getInt(cursor.getColumnIndex(SONG_ID)), cursor.getString(cursor.getColumnIndex(SONG_FILEPATH))); song.setTitle (cursor.getString(cursor.getColumnIndex(SONG_TITLE))); song.setArtist (cursor.getString(cursor.getColumnIndex(SONG_ARTIST))); song.setAlbum (cursor.getString(cursor.getColumnIndex(SONG_ALBUM))); song.setYear (cursor.getInt (cursor.getColumnIndex(SONG_YEAR))); song.setTrackNumber(cursor.getInt (cursor.getColumnIndex(SONG_TRACK_NO))); song.setDuration (cursor.getInt (cursor.getColumnIndex(SONG_DURATION))); // Using the previously created genre maps // to fill the current song genre. String currentGenreID = songIdToGenreIdMap.get(Long.toString(song.getId())); String currentGenreName = genreIdToGenreNameMap.get(currentGenreID); song.setGenre(currentGenreName); // Adding the song to the global list songs.add(song); } while (cursor.moveToNext()); } else { // What do I do if I can't find any songs? } cursor.close(); // Alright, now I'll get all the Playlists. // First I grab all playlist IDs and Names and then for each // one of those, getting all songs inside them. // As you know, the columns for the database. String PLAYLIST_ID = MediaStore.Audio.Playlists._ID; String PLAYLIST_NAME = MediaStore.Audio.Playlists.NAME; String PLAYLIST_SONG_ID = MediaStore.Audio.Playlists.Members.AUDIO_ID; // This is what I'll get for all playlists. String[] playlistColumns = { PLAYLIST_ID, PLAYLIST_NAME }; // The actual query - takes a while. cursor = resolver.query(playlistUri, playlistColumns, null, null, null); // Going through all playlists, creating my class and populating // it with all the song IDs they have. for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { Playlist playlist = new Playlist(cursor.getLong(cursor.getColumnIndex(PLAYLIST_ID)), cursor.getString(cursor.getColumnIndex(PLAYLIST_NAME))); // For each playlist, get all song IDs Uri currentUri = MediaStore.Audio.Playlists.Members.getContentUri(fromWhere, playlist.getID()); Cursor cursor2 = resolver.query(currentUri, new String[] { PLAYLIST_SONG_ID }, musicsOnly, null, null); // Adding each song's ID to it for (cursor2.moveToFirst(); !cursor2.isAfterLast(); cursor2.moveToNext()) playlist.add(cursor2.getLong(cursor2.getColumnIndex(PLAYLIST_SONG_ID))); playlists.add(playlist); cursor2.close(); } // Finally, let's sort the song list alphabetically // based on the song title. Collections.sort(songs, new Comparator<Song>() { public int compare(Song a, Song b) { return a.getTitle().compareTo(b.getTitle()); } }); scannedSongs = true; scanningSongs = false; } public void destroy() { songs.clear(); } /** * Returns an alphabetically sorted list with all the * artists of the scanned songs. * * @note This method might take a while depending on how * many songs you have. */ public ArrayList<String> getArtists() { ArrayList<String> artists = new ArrayList<String>(); for (Song song : songs) { String artist = song.getArtist(); if ((artist != null) && (! artists.contains(artist))) artists.add(artist); } // Making them alphabetically sorted Collections.sort(artists); return artists; } /** * Returns an alphabetically sorted list with all the * albums of the scanned songs. * * @note This method might take a while depending on how * many songs you have. */ public ArrayList<String> getAlbums() { ArrayList<String> albums = new ArrayList<String>(); for (Song song : songs) { String album = song.getAlbum(); if ((album != null) && (! albums.contains(album))) albums.add(album); } // Making them alphabetically sorted Collections.sort(albums); return albums; } /** * Returns an alphabetically sorted list with all * existing genres on the scanned songs. */ public ArrayList<String> getGenres() { ArrayList<String> genres = new ArrayList<String>(); for (String genre : genreIdToGenreNameMap.values()) genres.add(genre); Collections.sort(genres); return genres; } /** * Returns a list with all years your songs have. * * @note It is a list of Strings. To access the * years, do a `Integer.parseInt(string)`. */ public ArrayList<String> getYears() { ArrayList<String> years = new ArrayList<String>(); for (Song song : songs) { String year = Integer.toString(song.getYear()); if ((Integer.parseInt(year) > 0) && (! years.contains(year))) years.add(year); } // Making them alphabetically sorted Collections.sort(years); return years; } /** * Returns a list of Songs belonging to a specified artist. */ public ArrayList<Song> getSongsByArtist(String desiredArtist) { ArrayList<Song> songsByArtist = new ArrayList<Song>(); for (Song song : songs) { String currentArtist = song.getArtist(); if (currentArtist.equals(desiredArtist)) songsByArtist.add(song); } // Sorting resulting list by Album Collections.sort(songsByArtist, new Comparator<Song>() { public int compare(Song a, Song b) { return a.getAlbum().compareTo(b.getAlbum()); } }); return songsByArtist; } /** * Returns a list of album names belonging to a specified artist. */ public ArrayList<String> getAlbumsByArtist(String desiredArtist) { ArrayList<String> albumsByArtist = new ArrayList<String>(); for (Song song : songs) { String currentArtist = song.getArtist(); String currentAlbum = song.getAlbum(); if (currentArtist.equals(desiredArtist)) if (! albumsByArtist.contains(currentAlbum)) albumsByArtist.add(currentAlbum); } // Sorting alphabetically Collections.sort(albumsByArtist); return albumsByArtist; } /** * Returns a new list with all songs. * * @note This is different than accessing `songs` directly * because it duplicates it - you can then mess with * it without worrying about changing the original. */ public ArrayList<Song> getSongs() { ArrayList<Song> list = new ArrayList<Song>(); for (Song song : songs) list.add(song); return list; } /** * Returns a list of Songs belonging to a specified album. */ public ArrayList<Song> getSongsByAlbum(String desiredAlbum) { ArrayList<Song> songsByAlbum = new ArrayList<Song>(); for (Song song : songs) { String currentAlbum = song.getAlbum(); if (currentAlbum.equals(desiredAlbum)) songsByAlbum.add(song); } return songsByAlbum; } /** * Returns a list with all songs that have the same `genre.` */ public ArrayList<Song> getSongsByGenre(String genreName) { ArrayList<Song> currentSongs = new ArrayList<Song>(); for (Song song : songs) { String currentSongGenre = song.getGenre(); if (currentSongGenre == genreName) currentSongs.add(song); } return currentSongs; } /** * Returns a list with all songs composed at `year`. */ public ArrayList<Song> getSongsByYear(int year) { ArrayList<Song> currentSongs = new ArrayList<Song>(); for (Song song : songs) { int currentYear = song.getYear(); if (currentYear == year) currentSongs.add(song); } return currentSongs; } /** * * @return a list of playlist name */ public ArrayList<String> getPlaylistNames() { ArrayList<String> names = new ArrayList<String>(); for (Playlist playlist : playlists) names.add(playlist.getName()); return names; } /** * @return a song object if exists , null otherwise */ public Song getSongById(long id) { Song currentSong = null; for (Song song : songs) if (song.getId() == id) { currentSong = song; break; } return currentSong; } //@return song objects of a desired playlist or Null public ArrayList<Song> getSongsByPlaylist(String playlistName) { ArrayList<Long> songIDs = null; for (Playlist playlist : playlists) if (playlist.getName().equals(playlistName)) { songIDs = playlist.getSongIds(); break; } ArrayList<Song> currentSongs = new ArrayList<Song>(); for (Long songID : songIDs) currentSongs.add(getSongById(songID)); return currentSongs; } /** * Creates a new Playlist. * * @param c Activity on which we're creating. * @param fromWhere "internal" or "external". * @param name Playlist name. * @param songsToAdd List of song IDs to place on it. */ public void newPlaylist(Context c, String fromWhere, String name, ArrayList<Song> songsToAdd) { ContentResolver resolver = c.getContentResolver(); Uri playlistUri = ((fromWhere == "internal") ? android.provider.MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI: android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI); // CHECK IF PLAYLIST EXISTS! // Setting the new playlists' values ContentValues values = new ContentValues(); values.put(MediaStore.Audio.Playlists.NAME, name); values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, System.currentTimeMillis()); // Actually inserting the new playlist. Uri newPlaylistUri = resolver.insert(playlistUri, values); // Getting the new Playlist ID String PLAYLIST_ID = MediaStore.Audio.Playlists._ID; String PLAYLIST_NAME = MediaStore.Audio.Playlists.NAME; // This is what I'll get for all playlists. String[] playlistColumns = { PLAYLIST_ID, PLAYLIST_NAME }; // The actual query - takes a while. Cursor cursor = resolver.query(playlistUri, playlistColumns, null, null, null); long playlistID = 0; // Going through all playlists, creating my class and populating // it with all the song IDs they have. for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) if (name.equals(cursor.getString(cursor.getColumnIndex(PLAYLIST_NAME)))) playlistID = cursor.getLong(cursor.getColumnIndex(PLAYLIST_ID)); // Now, to it's songs Uri songUri = Uri.withAppendedPath(newPlaylistUri, MediaStore.Audio.Playlists.Members.CONTENT_DIRECTORY); int songOrder = 1; for (Song song : songsToAdd) { ContentValues songValues = new ContentValues(); songValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, song.getId()); songValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, songOrder); resolver.insert(songUri, songValues); songOrder++; } // Finally, we're updating our internal list of Playlists Playlist newPlaylist = new Playlist(playlistID, name); for (Song song : songsToAdd) newPlaylist.add(song.getId()); playlists.add(newPlaylist); } }