1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.music.utils; 18 19 import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 20 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.graphics.BitmapFactory; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.media.MediaActionSound; 31 import android.media.MediaMetadata; 32 import android.media.MediaMetadataRetriever; 33 import android.net.Uri; 34 import android.os.AsyncTask; 35 import android.provider.MediaStore; 36 import android.util.Log; 37 import com.android.music.MediaPlaybackService; 38 import com.android.music.MusicUtils; 39 import com.android.music.R; 40 41 import java.io.File; 42 import java.util.*; 43 import java.util.concurrent.ConcurrentHashMap; 44 import java.util.concurrent.ConcurrentMap; 45 46 /* 47 A provider of music contents to the music application, it reads external storage for any music 48 files, parse them and 49 store them in this class for future use. 50 */ 51 public class MusicProvider { 52 private static final String TAG = "MusicProvider"; 53 54 // Public constants 55 public static final String UNKOWN = "UNKNOWN"; 56 // Uri source of this track 57 public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__"; 58 // Sort key for this tack 59 public static final String CUSTOM_METADATA_SORT_KEY = "__SORT_KEY__"; 60 61 // Content select criteria 62 private static final String MUSIC_SELECT_FILTER = MediaStore.Audio.Media.IS_MUSIC + " != 0"; 63 private static final String MUSIC_SORT_ORDER = MediaStore.Audio.Media.TITLE + " ASC"; 64 65 // Categorized caches for music track data: 66 private Context mContext; 67 // Album Name --> list of Metadata 68 private ConcurrentMap<String, List<MediaMetadata>> mMusicListByAlbum; 69 // Playlist Name --> list of Metadata 70 private ConcurrentMap<String, List<MediaMetadata>> mMusicListByPlaylist; 71 // Artist Name --> Map of (album name --> album metadata) 72 private ConcurrentMap<String, Map<String, MediaMetadata>> mArtistAlbumDb; 73 private List<MediaMetadata> mMusicList; 74 private final ConcurrentMap<Long, Song> mMusicListById; 75 private final ConcurrentMap<String, Song> mMusicListByMediaId; 76 77 enum State { NON_INITIALIZED, INITIALIZING, INITIALIZED } 78 79 private volatile State mCurrentState = State.NON_INITIALIZED; 80 81 public MusicProvider(Context context) { 82 mContext = context; 83 mArtistAlbumDb = new ConcurrentHashMap<>(); 84 mMusicListByAlbum = new ConcurrentHashMap<>(); 85 mMusicListByPlaylist = new ConcurrentHashMap<>(); 86 mMusicListById = new ConcurrentHashMap<>(); 87 mMusicList = new ArrayList<>(); 88 mMusicListByMediaId = new ConcurrentHashMap<>(); 89 mMusicListByPlaylist.put(MediaIDHelper.MEDIA_ID_NOW_PLAYING, new ArrayList<>()); 90 } 91 92 public boolean isInitialized() { 93 return mCurrentState == State.INITIALIZED; 94 } 95 96 /** 97 * Get an iterator over the list of artists 98 * 99 * @return list of artists 100 */ 101 public Iterable<String> getArtists() { 102 if (mCurrentState != State.INITIALIZED) { 103 return Collections.emptyList(); 104 } 105 return mArtistAlbumDb.keySet(); 106 } 107 108 /** 109 * Get an iterator over the list of albums 110 * 111 * @return list of albums 112 */ 113 public Iterable<MediaMetadata> getAlbums() { 114 if (mCurrentState != State.INITIALIZED) { 115 return Collections.emptyList(); 116 } 117 ArrayList<MediaMetadata> albumList = new ArrayList<>(); 118 for (Map<String, MediaMetadata> artist_albums : mArtistAlbumDb.values()) { 119 albumList.addAll(artist_albums.values()); 120 } 121 return albumList; 122 } 123 124 /** 125 * Get an iterator over the list of playlists 126 * 127 * @return list of playlists 128 */ 129 public Iterable<String> getPlaylists() { 130 if (mCurrentState != State.INITIALIZED) { 131 return Collections.emptyList(); 132 } 133 return mMusicListByPlaylist.keySet(); 134 } 135 136 public Iterable<MediaMetadata> getMusicList() { 137 return mMusicList; 138 } 139 140 /** 141 * Get albums of a certain artist 142 * 143 */ 144 public Iterable<MediaMetadata> getAlbumByArtist(String artist) { 145 if (mCurrentState != State.INITIALIZED || !mArtistAlbumDb.containsKey(artist)) { 146 return Collections.emptyList(); 147 } 148 return mArtistAlbumDb.get(artist).values(); 149 } 150 151 /** 152 * Get music tracks of the given album 153 * 154 */ 155 public Iterable<MediaMetadata> getMusicsByAlbum(String album) { 156 if (mCurrentState != State.INITIALIZED || !mMusicListByAlbum.containsKey(album)) { 157 return Collections.emptyList(); 158 } 159 return mMusicListByAlbum.get(album); 160 } 161 162 /** 163 * Get music tracks of the given playlist 164 * 165 */ 166 public Iterable<MediaMetadata> getMusicsByPlaylist(String playlist) { 167 if (mCurrentState != State.INITIALIZED || !mMusicListByPlaylist.containsKey(playlist)) { 168 return Collections.emptyList(); 169 } 170 return mMusicListByPlaylist.get(playlist); 171 } 172 173 /** 174 * Return the MediaMetadata for the given musicID. 175 * 176 * @param musicId The unique, non-hierarchical music ID. 177 */ 178 public Song getMusicById(long musicId) { 179 return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null; 180 } 181 182 /** 183 * Return the MediaMetadata for the given musicID. 184 * 185 * @param musicId The unique, non-hierarchical music ID. 186 */ 187 public Song getMusicByMediaId(String musicId) { 188 return mMusicListByMediaId.containsKey(musicId) ? mMusicListByMediaId.get(musicId) : null; 189 } 190 191 /** 192 * Very basic implementation of a search that filter music tracks which title containing 193 * the given query. 194 * 195 */ 196 public Iterable<MediaMetadata> searchMusic(String titleQuery) { 197 if (mCurrentState != State.INITIALIZED) { 198 return Collections.emptyList(); 199 } 200 ArrayList<MediaMetadata> result = new ArrayList<>(); 201 titleQuery = titleQuery.toLowerCase(); 202 for (Song song : mMusicListByMediaId.values()) { 203 if (song.getMetadata() 204 .getString(MediaMetadata.METADATA_KEY_TITLE) 205 .toLowerCase() 206 .contains(titleQuery)) { 207 result.add(song.getMetadata()); 208 } 209 } 210 return result; 211 } 212 213 public interface MusicProviderCallback { void onMusicCatalogReady(boolean success); } 214 215 /** 216 * Get the list of music tracks from disk and caches the track information 217 * for future reference, keying tracks by musicId and grouping by genre. 218 */ 219 public void retrieveMediaAsync(final MusicProviderCallback callback) { 220 Log.d(TAG, "retrieveMediaAsync called"); 221 if (mCurrentState == State.INITIALIZED) { 222 // Nothing to do, execute callback immediately 223 callback.onMusicCatalogReady(true); 224 return; 225 } 226 227 // Asynchronously load the music catalog in a separate thread 228 new AsyncTask<Void, Void, State>() { 229 @Override 230 protected State doInBackground(Void... params) { 231 if (mCurrentState == State.INITIALIZED) { 232 return mCurrentState; 233 } 234 mCurrentState = State.INITIALIZING; 235 if (retrieveMedia()) { 236 mCurrentState = State.INITIALIZED; 237 } else { 238 mCurrentState = State.NON_INITIALIZED; 239 } 240 return mCurrentState; 241 } 242 243 @Override 244 protected void onPostExecute(State current) { 245 if (callback != null) { 246 callback.onMusicCatalogReady(current == State.INITIALIZED); 247 } 248 } 249 } 250 .execute(); 251 } 252 253 public synchronized boolean retrieveAllPlayLists() { 254 Cursor cursor = mContext.getContentResolver().query( 255 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, null, null, null); 256 if (cursor == null) { 257 Log.e(TAG, "Failed to retreive playlist: cursor is null"); 258 return false; 259 } 260 if (!cursor.moveToFirst()) { 261 Log.d(TAG, "Failed to move cursor to first row (no query result)"); 262 cursor.close(); 263 return true; 264 } 265 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists._ID); 266 int nameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.NAME); 267 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.DATA); 268 do { 269 long thisId = cursor.getLong(idColumn); 270 String thisPath = cursor.getString(pathColumn); 271 String thisName = cursor.getString(nameColumn); 272 Log.i(TAG, "PlayList ID: " + thisId + " Name: " + thisName); 273 List<MediaMetadata> songList = retreivePlaylistMetadata(thisId, thisPath); 274 LogHelper.i(TAG, "Found ", songList.size(), " items for playlist name: ", thisName); 275 mMusicListByPlaylist.put(thisName, songList); 276 } while (cursor.moveToNext()); 277 cursor.close(); 278 return true; 279 } 280 281 public synchronized List<MediaMetadata> retreivePlaylistMetadata( 282 long playlistId, String playlistPath) { 283 Cursor cursor = mContext.getContentResolver().query(Uri.parse(playlistPath), null, 284 MediaStore.Audio.Playlists.Members.PLAYLIST_ID + " == " + playlistId, null, null); 285 if (cursor == null) { 286 Log.e(TAG, "Failed to retreive individual playlist: cursor is null"); 287 return null; 288 } 289 if (!cursor.moveToFirst()) { 290 Log.d(TAG, "Failed to move cursor to first row (no query result for playlist)"); 291 cursor.close(); 292 return null; 293 } 294 List<Song> songList = new ArrayList<>(); 295 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members._ID); 296 int audioIdColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 297 int orderColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 298 int audioPathColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.DATA); 299 int audioNameColumn = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.TITLE); 300 do { 301 long thisId = cursor.getLong(idColumn); 302 long thisAudioId = cursor.getLong(audioIdColumn); 303 long thisOrder = cursor.getLong(orderColumn); 304 String thisAudioPath = cursor.getString(audioPathColumn); 305 Log.i(TAG, 306 "Playlist ID: " + playlistId + " Music ID: " + thisAudioId 307 + " Name: " + audioNameColumn); 308 if (!mMusicListById.containsKey(thisAudioId)) { 309 LogHelper.d(TAG, "Music does not exist"); 310 continue; 311 } 312 Song song = mMusicListById.get(thisAudioId); 313 song.setSortKey(thisOrder); 314 songList.add(song); 315 } while (cursor.moveToNext()); 316 cursor.close(); 317 songList.sort(new Comparator<Song>() { 318 @Override 319 public int compare(Song s1, Song s2) { 320 long key1 = s1.getSortKey(); 321 long key2 = s2.getSortKey(); 322 if (key1 < key2) { 323 return -1; 324 } else if (key1 == key2) { 325 return 0; 326 } else { 327 return 1; 328 } 329 } 330 }); 331 List<MediaMetadata> metadataList = new ArrayList<>(); 332 for (Song song : songList) { 333 metadataList.add(song.getMetadata()); 334 } 335 return metadataList; 336 } 337 338 private synchronized boolean retrieveMedia() { 339 if (mContext.checkSelfPermission(READ_EXTERNAL_STORAGE) 340 != PackageManager.PERMISSION_GRANTED) { 341 return false; 342 } 343 344 Cursor cursor = 345 mContext.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 346 null, MUSIC_SELECT_FILTER, null, MUSIC_SORT_ORDER); 347 if (cursor == null) { 348 Log.e(TAG, "Failed to retreive music: cursor is null"); 349 mCurrentState = State.NON_INITIALIZED; 350 return false; 351 } 352 if (!cursor.moveToFirst()) { 353 Log.d(TAG, "Failed to move cursor to first row (no query result)"); 354 cursor.close(); 355 return true; 356 } 357 int idColumn = cursor.getColumnIndex(MediaStore.Audio.Media._ID); 358 int titleColumn = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE); 359 int pathColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA); 360 do { 361 Log.i(TAG, 362 "Music ID: " + cursor.getString(idColumn) 363 + " Title: " + cursor.getString(titleColumn)); 364 long thisId = cursor.getLong(idColumn); 365 String thisPath = cursor.getString(pathColumn); 366 MediaMetadata metadata = retrievMediaMetadata(thisId, thisPath); 367 Log.i(TAG, "MediaMetadata: " + metadata); 368 if (metadata == null) { 369 continue; 370 } 371 Song thisSong = new Song(thisId, metadata, null); 372 // Construct per feature database 373 mMusicList.add(metadata); 374 mMusicListById.put(thisId, thisSong); 375 mMusicListByMediaId.put(String.valueOf(thisId), thisSong); 376 addMusicToAlbumList(metadata); 377 addMusicToArtistList(metadata); 378 } while (cursor.moveToNext()); 379 cursor.close(); 380 return true; 381 } 382 383 private synchronized MediaMetadata retrievMediaMetadata(long musicId, String musicPath) { 384 LogHelper.d(TAG, "getting metadata for music: ", musicPath); 385 MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 386 Uri contentUri = ContentUris.withAppendedId( 387 android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, musicId); 388 if (!(new File(musicPath).exists())) { 389 LogHelper.d(TAG, "Does not exist, deleting item"); 390 mContext.getContentResolver().delete(contentUri, null, null); 391 return null; 392 } 393 retriever.setDataSource(mContext, contentUri); 394 String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); 395 String album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); 396 String artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); 397 String durationString = 398 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 399 long duration = durationString != null ? Long.parseLong(durationString) : 0; 400 MediaMetadata.Builder metadataBuilder = 401 new MediaMetadata.Builder() 402 .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, String.valueOf(musicId)) 403 .putString(CUSTOM_METADATA_TRACK_SOURCE, musicPath) 404 .putString(MediaMetadata.METADATA_KEY_TITLE, title != null ? title : UNKOWN) 405 .putString(MediaMetadata.METADATA_KEY_ALBUM, album != null ? album : UNKOWN) 406 .putString( 407 MediaMetadata.METADATA_KEY_ARTIST, artist != null ? artist : UNKOWN) 408 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); 409 byte[] albumArtData = retriever.getEmbeddedPicture(); 410 Bitmap bitmap; 411 if (albumArtData != null) { 412 bitmap = BitmapFactory.decodeByteArray(albumArtData, 0, albumArtData.length); 413 bitmap = MusicUtils.resizeBitmap(bitmap, getDefaultAlbumArt()); 414 metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap); 415 } 416 retriever.release(); 417 return metadataBuilder.build(); 418 } 419 420 private Bitmap getDefaultAlbumArt() { 421 BitmapFactory.Options opts = new BitmapFactory.Options(); 422 opts.inPreferredConfig = Bitmap.Config.ARGB_8888; 423 return BitmapFactory.decodeStream( 424 mContext.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, 425 opts); 426 } 427 428 private void addMusicToAlbumList(MediaMetadata metadata) { 429 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); 430 if (thisAlbum == null) { 431 thisAlbum = UNKOWN; 432 } 433 if (!mMusicListByAlbum.containsKey(thisAlbum)) { 434 mMusicListByAlbum.put(thisAlbum, new ArrayList<>()); 435 } 436 mMusicListByAlbum.get(thisAlbum).add(metadata); 437 } 438 439 private void addMusicToArtistList(MediaMetadata metadata) { 440 String thisArtist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST); 441 if (thisArtist == null) { 442 thisArtist = UNKOWN; 443 } 444 String thisAlbum = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM); 445 if (thisAlbum == null) { 446 thisAlbum = UNKOWN; 447 } 448 if (!mArtistAlbumDb.containsKey(thisArtist)) { 449 mArtistAlbumDb.put(thisArtist, new ConcurrentHashMap<>()); 450 } 451 Map<String, MediaMetadata> albumsMap = mArtistAlbumDb.get(thisArtist); 452 MediaMetadata.Builder builder; 453 long count = 0; 454 Bitmap thisAlbumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); 455 if (albumsMap.containsKey(thisAlbum)) { 456 MediaMetadata album_metadata = albumsMap.get(thisAlbum); 457 count = album_metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); 458 Bitmap nAlbumArt = album_metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); 459 builder = new MediaMetadata.Builder(album_metadata); 460 if (nAlbumArt != null) { 461 thisAlbumArt = null; 462 } 463 } else { 464 builder = new MediaMetadata.Builder(); 465 builder.putString(MediaMetadata.METADATA_KEY_ALBUM, thisAlbum) 466 .putString(MediaMetadata.METADATA_KEY_ARTIST, thisArtist); 467 } 468 if (thisAlbumArt != null) { 469 builder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thisAlbumArt); 470 } 471 builder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, count + 1); 472 albumsMap.put(thisAlbum, builder.build()); 473 } 474 475 public synchronized void updateMusic(String musicId, MediaMetadata metadata) { 476 Song song = mMusicListByMediaId.get(musicId); 477 if (song == null) { 478 return; 479 } 480 481 String oldGenre = song.getMetadata().getString(MediaMetadata.METADATA_KEY_GENRE); 482 String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE); 483 484 song.setMetadata(metadata); 485 486 // if genre has changed, we need to rebuild the list by genre 487 if (!oldGenre.equals(newGenre)) { 488 // buildListsByGenre(); 489 } 490 } 491 } 492