DataModel.java revision 7e37ae05a9c7c9ecf8569a5bfe752a54f660a6ba
1a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono/* 2a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Copyright (c) 2016, The Android Open Source Project 3a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * 4a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Licensed under the Apache License, Version 2.0 (the "License"); 5a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * you may not use this file except in compliance with the License. 6a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * You may obtain a copy of the License at 7a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * 8a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * http://www.apache.org/licenses/LICENSE-2.0 9a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * 10a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Unless required by applicable law or agreed to in writing, software 11a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * distributed under the License is distributed on an "AS IS" BASIS, 12a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * See the License for the specific language governing permissions and 14a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * limitations under the License. 15a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono */ 16a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironopackage com.android.car.media.localmediaplayer; 17a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 18a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.ContentResolver; 19a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.ContentUris; 20a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.content.Context; 21a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.database.Cursor; 22a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.database.sqlite.SQLiteException; 23a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.MediaDescription; 24a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.MediaMetadata; 25a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.media.browse.MediaBrowser.MediaItem; 26f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.media.session.MediaSession.QueueItem; 27f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.net.Uri; 28f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport android.os.AsyncTask; 29a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.os.Bundle; 30a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore; 31a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore.Audio.AlbumColumns; 32a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.provider.MediaStore.Audio.AudioColumns; 33a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.service.media.MediaBrowserService.Result; 34a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport android.util.Log; 35a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 36a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.File; 37a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.FileNotFoundException; 38a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.IOException; 39a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.io.InputStream; 40a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.ArrayList; 41a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.HashSet; 42a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hironoimport java.util.List; 43f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironoimport java.util.Set; 44a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 45f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hironopublic class DataModel { 46f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private static final String TAG = "LMBDataModel"; 47f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 48a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static final Uri[] ALL_AUDIO_URI = new Uri[] { 49a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Media.INTERNAL_CONTENT_URI, 50f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 51f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono }; 52f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 53a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static final Uri[] ALBUMS_URI = new Uri[] { 54a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, 55f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI 56f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono }; 57f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 58a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static final Uri[] ARTISTS_URI = new Uri[] { 59a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, 60f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI 61f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono }; 62f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 63f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private static final Uri[] GENRES_URI = new Uri[] { 64a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, 65a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI 66f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono }; 67f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 68f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private static final String QUERY_BY_KEY_WHERE_CLAUSE = 69f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono AudioColumns.ALBUM_KEY + "= ? or " 70a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono + AudioColumns.ARTIST_KEY + " = ? or " 71a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono + AudioColumns.TITLE_KEY + " = ? or " 72f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono + AudioColumns.DATA + " like ?"; 73f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 74f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private static final String EXTERNAL = "external"; 75a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static final String INTERNAL = "internal"; 76a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 77f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private static final Uri ART_BASE_URI = Uri.parse("content://media/external/audio/albumart"); 78f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono // Need a context to create this constant so it can't be static. 79f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private final String DEFAULT_ALBUM_ART_URI; 80a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 81a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public static final String PATH_KEY = "PATH"; 82a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 83a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private Context mContext; 84a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private ContentResolver mResolver; 85f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private AsyncTask mPendingTask; 86f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 87f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono private List<QueueItem> mQueue = new ArrayList<>(); 88f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 89f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono public DataModel(Context context) { 90f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono mContext = context; 91f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono mResolver = context.getContentResolver(); 92f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono DEFAULT_ALBUM_ART_URI = 93f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono Utils.getUriForResource(context, R.drawable.ic_sd_storage_black).toString(); 94f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono } 95f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 96f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono public void onQueryByFolder(String parentId, Result<List<MediaItem>> result) { 97f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver); 98a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono queryInBackground(result, query); 99a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 100a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 101a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public void onQueryByAlbum(String parentId, Result<List<MediaItem>> result) { 102a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono QueryTask query = new QueryTask.Builder() 103a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResolver(mResolver) 104a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResult(result) 105a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setUri(ALBUMS_URI) 106a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setKeyColumn(AudioColumns.ALBUM_KEY) 107a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setTitleColumn(AudioColumns.ALBUM) 108a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setFlags(MediaItem.FLAG_BROWSABLE) 109a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .build(); 110a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono queryInBackground(result, query); 111a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 112a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 113a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public void onQueryByArtist(String parentId, Result<List<MediaItem>> result) { 114a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono QueryTask query = new QueryTask.Builder() 115a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResolver(mResolver) 116a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResult(result) 117a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setUri(ARTISTS_URI) 118a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setKeyColumn(AudioColumns.ARTIST_KEY) 119a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setTitleColumn(AudioColumns.ARTIST) 120a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setFlags(MediaItem.FLAG_BROWSABLE) 121a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .build(); 122a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono queryInBackground(result, query); 123a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 124a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 125a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public void onQueryByGenre(String parentId, Result<List<MediaItem>> result) { 126a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono QueryTask query = new QueryTask.Builder() 127a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResolver(mResolver) 128a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResult(result) 129a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setUri(GENRES_URI) 130a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setKeyColumn(MediaStore.Audio.Genres._ID) 131a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setTitleColumn(MediaStore.Audio.Genres.NAME) 132a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setFlags(MediaItem.FLAG_BROWSABLE) 133a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .build(); 134a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono queryInBackground(result, query); 135a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 136a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 137a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private void queryInBackground(Result<List<MediaItem>> result, 138a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono AsyncTask<Void, Void, Void> task) { 139a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono result.detach(); 140a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 141a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (mPendingTask != null) { 142a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mPendingTask.cancel(true); 143a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 144a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 145a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mPendingTask = task; 146a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono task.execute(); 147a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 148a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 149a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public List<QueueItem> getQueue() { 150a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono return mQueue; 151a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 152a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 153a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public MediaMetadata getMetadata(String key) { 154a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Cursor cursor = null; 155a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaMetadata.Builder metadata = new MediaMetadata.Builder(); 156a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono try { 157a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono for (Uri uri : ALL_AUDIO_URI) { 158a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor = mResolver.query(uri, null, AudioColumns.TITLE_KEY + " = ?", 159a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono new String[]{ key }, null); 160a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (cursor != null) { 161a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int title = cursor.getColumnIndex(AudioColumns.TITLE); 162a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int artist = cursor.getColumnIndex(AudioColumns.ARTIST); 163a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int album = cursor.getColumnIndex(AudioColumns.ALBUM); 164a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID); 165a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int duration = cursor.getColumnIndex(AudioColumns.DURATION); 166a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 167a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono while (cursor.moveToNext()) { 168a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono metadata.putString(MediaMetadata.METADATA_KEY_TITLE, 169a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.getString(title)); 170a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono metadata.putString(MediaMetadata.METADATA_KEY_ARTIST, 171a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.getString(artist)); 172a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono metadata.putString(MediaMetadata.METADATA_KEY_ALBUM, 173a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.getString(album)); 174a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono metadata.putLong(MediaMetadata.METADATA_KEY_DURATION, 175a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.getLong(duration)); 176a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 177a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono String albumArt = DEFAULT_ALBUM_ART_URI; 178a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI, 179a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.getLong(albumId)); 180a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono try { 181a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono InputStream dummy = mResolver.openInputStream(albumArtUri); 182a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono albumArt = albumArtUri.toString(); 183a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono dummy.close(); 184a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } catch (IOException e) { 185a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // Ignored because the albumArt is intialized correctly anyway. 186a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 187a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt); 188a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono break; 189a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 190a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 191a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 192a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } finally { 193a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (cursor != null) { 194a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor.close(); 195a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 196a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 197a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 198a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono return metadata.build(); 199a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 200a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 201a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono /** 202a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * Note: This clears out the queue. You should have a local copy of the queue before calling 203a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono * this method. 204a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono */ 205a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public void onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result) { 206a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mQueue.clear(); 207a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 208a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono QueryTask.Builder query = new QueryTask.Builder() 209a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResolver(mResolver) 210a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setResult(result); 211a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 212a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Uri[] uri = null; 213a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (lastCategory.equals(LocalMediaBrowserService.GENRES_ID)) { 214a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // Genres come from a different table and don't use the where clause from the 215a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // usual media table so we need to have this condition. 216a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono try { 217a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono long id = Long.parseLong(parentId); 218a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono query.setUri(new Uri[] { 219a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id), 220a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) }); 221a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } catch (NumberFormatException e) { 222a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // This should never happen. 223a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result"); 224a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono result.sendResult(new ArrayList<MediaItem>()); 225a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono return; 226a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 227a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } else { 228a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono query.setUri(ALL_AUDIO_URI) 229a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE) 230a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setWhereArgs(new String[] { parentId, parentId, parentId, parentId }); 231a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 232a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 233a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono query.setKeyColumn(AudioColumns.TITLE_KEY) 234a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setTitleColumn(AudioColumns.TITLE) 235a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setSubtitleColumn(AudioColumns.ALBUM) 236a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setFlags(MediaItem.FLAG_PLAYABLE) 237a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setQueue(mQueue); 238a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono queryInBackground(result, query.build()); 239a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 240a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 241a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // This async task is similar enough to all the others that it feels like it can be unified 242a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // but is different enough that unifying it makes the code for both cases look really weird 243a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // and over paramterized so at the risk of being a little more verbose, this is separated out 244a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // in the name of understandability. 245a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static class FilesystemListTask extends AsyncTask<Void, Void, Void> { 246a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private static final String[] COLUMNS = { AudioColumns.DATA }; 247a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private Result<List<MediaItem>> mResult; 248a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private Uri[] mUris; 249a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono private ContentResolver mResolver; 250a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 251a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono public FilesystemListTask(Result<List<MediaItem>> result, Uri[] uris, 252a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono ContentResolver resolver) { 253a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mResult = result; 254a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mUris = uris; 255a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono mResolver = resolver; 256a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 257a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 258a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono @Override 259a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono protected Void doInBackground(Void... voids) { 260a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Set<String> paths = new HashSet<String>(); 261a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 262a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Cursor cursor = null; 263a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono for (Uri uri : mUris) { 264a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono try { 265a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono cursor = mResolver.query(uri, COLUMNS, null , null, null); 266a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (cursor != null) { 267a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int pathColumn = cursor.getColumnIndex(AudioColumns.DATA); 268a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 269a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono while (cursor.moveToNext()) { 270a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // We want to de-dupe paths of each of the songs so we get just a list 271a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono // of containing directories. 272a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono String fullPath = cursor.getString(pathColumn); 273a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono int fileNameStart = fullPath.lastIndexOf(File.separator); 274a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono if (fileNameStart < 0) { 275a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono continue; 276a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 277a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono 278a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono String dirPath = fullPath.substring(0, fileNameStart); 279a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono paths.add(dirPath); 280a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 281a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } 282a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } catch (SQLiteException e) { 283a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono Log.e(TAG, "Failed to execute query " + e); // Stack trace is noisy. 284a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono } finally { 285f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono if (cursor != null) { 286f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono cursor.close(); 287f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono } 288f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono } 289f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono } 290f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono 291f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono // Take the list of deduplicated directories and put them into the results list with 292f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono // the full directory path as the key so we can match on it later. 293f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono List<MediaItem> results = new ArrayList<>(); 294f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono for (String path : paths) { 295f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono int dirNameStart = path.lastIndexOf(File.separator) + 1; 296f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono String dirName = path.substring(dirNameStart, path.length()); 297f5d15f9fc4b8bd7a866660fe208bf857dea839baDaichi Hirono MediaDescription description = new MediaDescription.Builder() 298a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setMediaId(path + "%") // Used in a like query. 299a0aecda12b9a76aa15a8c5175e15538574a05af7Daichi Hirono .setTitle(dirName) 300 .setSubtitle(path) 301 .build(); 302 results.add(new MediaItem(description, MediaItem.FLAG_BROWSABLE)); 303 } 304 mResult.sendResult(results); 305 return null; 306 } 307 } 308 309 private static class QueryTask extends AsyncTask<Void, Void, Void> { 310 private Result<List<MediaItem>> mResult; 311 private String[] mColumns; 312 private String mWhereClause; 313 private String[] mWhereArgs; 314 private String mKeyColumn; 315 private String mTitleColumn; 316 private String mSubtitleColumn; 317 private Uri[] mUris; 318 private int mFlags; 319 private ContentResolver mResolver; 320 private List<QueueItem> mQueue; 321 322 private QueryTask(Builder builder) { 323 mColumns = builder.mColumns; 324 mWhereClause = builder.mWhereClause; 325 mWhereArgs = builder.mWhereArgs; 326 mKeyColumn = builder.mKeyColumn; 327 mTitleColumn = builder.mTitleColumn; 328 mUris = builder.mUris; 329 mFlags = builder.mFlags; 330 mResolver = builder.mResolver; 331 mResult = builder.mResult; 332 mQueue = builder.mQueue; 333 mSubtitleColumn = builder.mSubtitleColumn; 334 } 335 336 @Override 337 protected Void doInBackground(Void... voids) { 338 List<MediaItem> results = new ArrayList<>(); 339 340 long idx = 0; 341 342 Cursor cursor = null; 343 for (Uri uri : mUris) { 344 try { 345 cursor = mResolver.query(uri, mColumns, mWhereClause, mWhereArgs, null); 346 if (cursor != null) { 347 int keyColumn = cursor.getColumnIndex(mKeyColumn); 348 int titleColumn = cursor.getColumnIndex(mTitleColumn); 349 int pathColumn = cursor.getColumnIndex(AudioColumns.DATA); 350 int subtitleColumn = -1; 351 if (mSubtitleColumn != null) { 352 subtitleColumn = cursor.getColumnIndex(mSubtitleColumn); 353 } 354 355 while (cursor.moveToNext()) { 356 Bundle path = new Bundle(); 357 if (pathColumn != -1) { 358 path.putString(PATH_KEY, cursor.getString(pathColumn)); 359 } 360 361 MediaDescription.Builder builder = new MediaDescription.Builder() 362 .setMediaId(cursor.getString(keyColumn)) 363 .setTitle(cursor.getString(titleColumn)) 364 .setExtras(path); 365 366 if (subtitleColumn != -1) { 367 builder.setSubtitle(cursor.getString(subtitleColumn)); 368 } 369 370 MediaDescription description = builder.build(); 371 results.add(new MediaItem(description, mFlags)); 372 373 // We rebuild the queue here so if the user selects the item then we 374 // can immediately use this queue. 375 if (mQueue != null) { 376 mQueue.add(new QueueItem(description, idx)); 377 } 378 idx++; 379 } 380 } 381 } catch (SQLiteException e) { 382 // Sometimes tables don't exist if the media scanner hasn't seen data of that 383 // type yet. For example, the genres table doesn't seem to exist at all until 384 // the first time a song with a genre is encountered. If we hit an exception, 385 // the result is never sent causing the other end to hang up, which is a bad 386 // thing. We can instead just be resilient and return an empty list. 387 Log.i(TAG, "Failed to execute query " + e); // Stack trace is noisy. 388 } finally { 389 if (cursor != null) { 390 cursor.close(); 391 } 392 } 393 } 394 395 mResult.sendResult(results); 396 return null; // Ignored. 397 } 398 399 // 400 // Boilerplate Alert! 401 // 402 public static class Builder { 403 private Result<List<MediaItem>> mResult; 404 private String[] mColumns; 405 private String mWhereClause; 406 private String[] mWhereArgs; 407 private String mKeyColumn; 408 private String mTitleColumn; 409 private String mSubtitleColumn; 410 private Uri[] mUris; 411 private int mFlags; 412 private ContentResolver mResolver; 413 private List<QueueItem> mQueue; 414 415 public Builder setColumns(String[] columns) { 416 mColumns = columns; 417 return this; 418 } 419 420 public Builder setWhereClause(String whereClause) { 421 mWhereClause = whereClause; 422 return this; 423 } 424 425 public Builder setWhereArgs(String[] whereArgs) { 426 mWhereArgs = whereArgs; 427 return this; 428 } 429 430 public Builder setUri(Uri[] uris) { 431 mUris = uris; 432 return this; 433 } 434 435 public Builder setKeyColumn(String keyColumn) { 436 mKeyColumn = keyColumn; 437 return this; 438 } 439 440 public Builder setTitleColumn(String titleColumn) { 441 mTitleColumn = titleColumn; 442 return this; 443 } 444 445 public Builder setSubtitleColumn(String subtitleColumn) { 446 mSubtitleColumn = subtitleColumn; 447 return this; 448 } 449 450 public Builder setFlags(int flags) { 451 mFlags = flags; 452 return this; 453 } 454 455 public Builder setResult(Result<List<MediaItem>> result) { 456 mResult = result; 457 return this; 458 } 459 460 public Builder setResolver(ContentResolver resolver) { 461 mResolver = resolver; 462 return this; 463 } 464 465 public Builder setQueue(List<QueueItem> queue) { 466 mQueue = queue; 467 return this; 468 } 469 470 public QueryTask build() { 471 if (mUris == null || mKeyColumn == null || mResolver == null || 472 mResult == null || mTitleColumn == null) { 473 throw new IllegalStateException( 474 "uri, keyColumn, resolver, result and titleColumn are required."); 475 } 476 return new QueryTask(this); 477 } 478 } 479 } 480} 481