1 /* 2 * Copyright 2018 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.pump.db; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.provider.MediaStore; 25 26 import androidx.annotation.AnyThread; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.WorkerThread; 30 31 import com.android.pump.util.Clog; 32 import com.android.pump.util.Collections; 33 34 import java.util.ArrayList; 35 import java.util.Collection; 36 37 @WorkerThread 38 class AudioStore extends ContentObserver { 39 private static final String TAG = Clog.tag(AudioStore.class); 40 41 private final ContentResolver mContentResolver; 42 private final ChangeListener mChangeListener; 43 private final MediaProvider mMediaProvider; 44 45 interface ChangeListener { onAudiosAdded(@onNull Collection<Audio> audios)46 void onAudiosAdded(@NonNull Collection<Audio> audios); onArtistsAdded(@onNull Collection<Artist> artists)47 void onArtistsAdded(@NonNull Collection<Artist> artists); onAlbumsAdded(@onNull Collection<Album> albums)48 void onAlbumsAdded(@NonNull Collection<Album> albums); onGenresAdded(@onNull Collection<Genre> genres)49 void onGenresAdded(@NonNull Collection<Genre> genres); onPlaylistsAdded(@onNull Collection<Playlist> playlists)50 void onPlaylistsAdded(@NonNull Collection<Playlist> playlists); 51 } 52 53 @AnyThread AudioStore(@onNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, @NonNull MediaProvider mediaProvider)54 AudioStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, 55 @NonNull MediaProvider mediaProvider) { 56 super(null); 57 58 Clog.i(TAG, "AudioStore(" + contentResolver + ", " + changeListener 59 + ", " + mediaProvider + ")"); 60 mContentResolver = contentResolver; 61 mChangeListener = changeListener; 62 mMediaProvider = mediaProvider; 63 64 // TODO(123705758) Do we need content observer for other content uris? (E.g. album, artist) 65 mContentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 66 true, this); 67 68 // TODO(123705758) When to call unregisterContentObserver? 69 // mContentResolver.unregisterContentObserver(this); 70 } 71 load()72 void load() { 73 Clog.i(TAG, "load()"); 74 ArrayList<Artist> artists = new ArrayList<>(); 75 ArrayList<Album> albums = new ArrayList<>(); 76 ArrayList<Audio> audios = new ArrayList<>(); 77 ArrayList<Playlist> playlists = new ArrayList<>(); 78 ArrayList<Genre> genres = new ArrayList<>(); 79 80 // #1 Load artists 81 { 82 Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; 83 String[] projection = { 84 MediaStore.Audio.Artists._ID 85 }; 86 String sortOrder = MediaStore.Audio.Artists._ID; 87 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 88 if (cursor != null) { 89 try { 90 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID); 91 92 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 93 long id = cursor.getLong(idColumn); 94 95 Artist artist = new Artist(id); 96 artists.add(artist); 97 } 98 } finally { 99 cursor.close(); 100 } 101 } 102 } 103 104 // #2 Load albums and connect each to artist 105 { 106 Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; 107 String[] projection = { 108 MediaStore.Audio.Albums._ID, 109 MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID 110 }; 111 String sortOrder = MediaStore.Audio.Albums._ID; 112 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 113 if (cursor != null) { 114 try { 115 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID); 116 int artistIdColumn = cursor.getColumnIndexOrThrow( 117 MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID 118 119 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 120 long id = cursor.getLong(idColumn); 121 122 Album album = new Album(id); 123 albums.add(album); 124 125 if (!cursor.isNull(artistIdColumn)) { 126 long artistId = cursor.getLong(artistIdColumn); 127 128 Artist artist = Collections.find(artists, artistId, Artist::getId); 129 album.setArtist(artist); 130 } 131 } 132 } finally { 133 cursor.close(); 134 } 135 } 136 } 137 138 // #3 Load songs and connect each to album and artist 139 { 140 Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 141 String[] projection = { 142 MediaStore.Audio.Media._ID, 143 MediaStore.Audio.Media.MIME_TYPE, 144 MediaStore.Audio.Media.ARTIST_ID, 145 MediaStore.Audio.Media.ALBUM_ID 146 }; 147 String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"; 148 String sortOrder = MediaStore.Audio.Media._ID; 149 Cursor cursor = mContentResolver.query(contentUri, projection, selection, null, sortOrder); 150 if (cursor != null) { 151 try { 152 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 153 int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE); 154 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); 155 int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); 156 157 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 158 long id = cursor.getLong(idColumn); 159 String mimeType = cursor.getString(mimeTypeColumn); 160 161 Audio audio = new Audio(id, mimeType); 162 audios.add(audio); 163 164 if (!cursor.isNull(artistIdColumn)) { 165 long artistId = cursor.getLong(artistIdColumn); 166 167 Artist artist = Collections.find(artists, artistId, Artist::getId); 168 audio.setArtist(artist); 169 artist.addAudio(audio); 170 } 171 if (!cursor.isNull(albumIdColumn)) { 172 long albumId = cursor.getLong(albumIdColumn); 173 174 Album album = Collections.find(albums, albumId, Album::getId); 175 audio.setAlbum(album); 176 album.addAudio(audio); 177 } 178 } 179 } finally { 180 cursor.close(); 181 } 182 } 183 } 184 185 // #4 Load playlists (optional?) 186 { 187 Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; 188 String[] projection = { 189 MediaStore.Audio.Playlists._ID 190 }; 191 String sortOrder = MediaStore.Audio.Playlists._ID; 192 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 193 if (cursor != null) { 194 try { 195 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID); 196 197 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 198 long id = cursor.getLong(idColumn); 199 200 Playlist playlist = new Playlist(id); 201 playlists.add(playlist); 202 } 203 } finally { 204 cursor.close(); 205 } 206 } 207 } 208 209 // #5 Load genres (optional?) 210 { 211 Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; 212 String[] projection = { 213 MediaStore.Audio.Genres._ID 214 }; 215 String sortOrder = MediaStore.Audio.Genres._ID; 216 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 217 if (cursor != null) { 218 try { 219 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID); 220 221 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 222 long id = cursor.getLong(idColumn); 223 224 Genre genre = new Genre(id); 225 genres.add(genre); 226 } 227 } finally { 228 cursor.close(); 229 } 230 } 231 } 232 233 mChangeListener.onAudiosAdded(audios); 234 mChangeListener.onArtistsAdded(artists); 235 mChangeListener.onAlbumsAdded(albums); 236 mChangeListener.onGenresAdded(genres); 237 mChangeListener.onPlaylistsAdded(playlists); 238 } 239 loadData(@onNull Audio audio)240 boolean loadData(@NonNull Audio audio) { 241 boolean updated = false; 242 243 Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 244 String[] projection = { 245 MediaStore.Audio.Media.TITLE, 246 MediaStore.Audio.Media.ARTIST_ID, 247 MediaStore.Audio.Media.ALBUM_ID 248 }; 249 String selection = MediaStore.Audio.Media._ID + " = ?"; 250 String[] selectionArgs = { Long.toString(audio.getId()) }; 251 Cursor cursor = mContentResolver.query( 252 contentUri, projection, selection, selectionArgs, null); 253 if (cursor != null) { 254 try { 255 int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); 256 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); 257 int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); 258 259 if (cursor.moveToFirst()) { 260 if (!cursor.isNull(titleColumn)) { 261 String title = cursor.getString(titleColumn); 262 updated |= audio.setTitle(title); 263 } 264 if (!cursor.isNull(artistIdColumn)) { 265 long artistId = cursor.getLong(artistIdColumn); 266 Artist artist = mMediaProvider.getArtistById(artistId); 267 updated |= audio.setArtist(artist); 268 updated |= loadData(artist); // TODO(b/123707561) Load separate from audio 269 } 270 if (!cursor.isNull(albumIdColumn)) { 271 long albumId = cursor.getLong(albumIdColumn); 272 Album album = mMediaProvider.getAlbumById(albumId); 273 updated |= audio.setAlbum(album); 274 updated |= loadData(album); // TODO(b/123707561) Load separate from audio 275 } 276 } 277 } finally { 278 cursor.close(); 279 } 280 } 281 282 return updated; 283 } 284 loadData(@onNull Artist artist)285 boolean loadData(@NonNull Artist artist) { 286 boolean updated = false; 287 288 Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; 289 String[] projection = { MediaStore.Audio.Artists.ARTIST }; 290 String selection = MediaStore.Audio.Artists._ID + " = ?"; 291 String[] selectionArgs = { Long.toString(artist.getId()) }; 292 Cursor cursor = mContentResolver.query( 293 contentUri, projection, selection, selectionArgs, null); 294 if (cursor != null) { 295 try { 296 int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST); 297 298 if (cursor.moveToFirst()) { 299 if (!cursor.isNull(artistColumn)) { 300 String name = cursor.getString(artistColumn); 301 updated |= artist.setName(name); 302 } 303 } 304 } finally { 305 cursor.close(); 306 } 307 } 308 309 updated |= loadAlbums(artist); // TODO(b/123707561) Load separate from artist 310 311 return updated; 312 } 313 loadData(@onNull Album album)314 boolean loadData(@NonNull Album album) { 315 boolean updated = false; 316 317 Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; 318 String[] projection = { 319 MediaStore.Audio.Albums.ALBUM, 320 MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID 321 }; 322 String selection = MediaStore.Audio.Albums._ID + " = ?"; 323 String[] selectionArgs = { Long.toString(album.getId()) }; 324 Cursor cursor = mContentResolver.query( 325 contentUri, projection, selection, selectionArgs, null); 326 if (cursor != null) { 327 try { 328 int albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM); 329 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID 330 331 if (cursor.moveToFirst()) { 332 if (!cursor.isNull(albumColumn)) { 333 String albumTitle = cursor.getString(albumColumn); 334 updated |= album.setTitle(albumTitle); 335 } 336 if (!cursor.isNull(artistIdColumn)) { 337 long artistId = cursor.getLong(artistIdColumn); 338 Artist artist = mMediaProvider.getArtistById(artistId); 339 updated |= album.setArtist(artist); 340 updated |= loadData(artist); // TODO(b/123707561) Load separate from album 341 } 342 343 // TODO(b/130363861) No need to store the URI -- generate when requested instead 344 Uri albumArtUri = new Uri.Builder() 345 .scheme(ContentResolver.SCHEME_CONTENT) 346 .authority(MediaStore.AUTHORITY) 347 .appendPath("external").appendPath("audio").appendPath("albumart") 348 .appendPath(Long.toString(album.getId())).build(); 349 updated |= album.setAlbumArtUri(albumArtUri); 350 } 351 } finally { 352 cursor.close(); 353 } 354 } 355 356 return updated; 357 } 358 loadData(@onNull Genre genre)359 boolean loadData(@NonNull Genre genre) { 360 boolean updated = false; 361 362 Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; 363 String[] projection = { MediaStore.Audio.Genres.NAME }; 364 String selection = MediaStore.Audio.Genres._ID + " = ?"; 365 String[] selectionArgs = { Long.toString(genre.getId()) }; 366 Cursor cursor = mContentResolver.query( 367 contentUri, projection, selection, selectionArgs, null); 368 if (cursor != null) { 369 try { 370 int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME); 371 372 if (cursor.moveToFirst()) { 373 if (!cursor.isNull(nameColumn)) { 374 String name = cursor.getString(nameColumn); 375 updated |= genre.setName(name); 376 } 377 } 378 } finally { 379 cursor.close(); 380 } 381 } 382 383 updated |= loadAudios(genre); // TODO(b/123707561) Load separate from genre 384 385 return updated; 386 } 387 loadData(@onNull Playlist playlist)388 boolean loadData(@NonNull Playlist playlist) { 389 boolean updated = false; 390 391 Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; 392 String[] projection = { MediaStore.Audio.Playlists.NAME }; 393 String selection = MediaStore.Audio.Playlists._ID + " = ?"; 394 String[] selectionArgs = { Long.toString(playlist.getId()) }; 395 Cursor cursor = mContentResolver.query( 396 contentUri, projection, selection, selectionArgs, null); 397 if (cursor != null) { 398 try { 399 int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME); 400 401 if (cursor.moveToFirst()) { 402 if (!cursor.isNull(nameColumn)) { 403 String name = cursor.getString(nameColumn); 404 updated |= playlist.setName(name); 405 } 406 } 407 } finally { 408 cursor.close(); 409 } 410 } 411 412 updated |= loadAudios(playlist); // TODO(b/123707561) Load separate from playlist 413 414 return updated; 415 } 416 loadAlbums(@onNull Artist artist)417 boolean loadAlbums(@NonNull Artist artist) { 418 boolean updated = false; 419 420 // TODO Remove hardcoded value 421 Uri contentUri = MediaStore.Audio.Artists.Albums.getContentUri("external", artist.getId()); 422 /* 423 * On some devices MediaStore doesn't use ALBUM_ID as key from Artist to Album, but rather 424 * _ID. In order to support these devices we don't pass a projection, to avoid the 425 * IllegalArgumentException(Invalid column) exception, and then resort to _ID. 426 */ 427 String[] projection = null; // { MediaStore.Audio.Artists.Albums.ALBUM_ID }; 428 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); 429 if (cursor != null) { 430 try { 431 int albumIdColumn = cursor.getColumnIndex(MediaStore.Audio.Artists.Albums.ALBUM_ID); 432 if (albumIdColumn < 0) { 433 // On some devices the ALBUM_ID column doesn't exist and _ID is used instead. 434 albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 435 } 436 437 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 438 long albumId = cursor.getLong(albumIdColumn); 439 Album album = mMediaProvider.getAlbumById(albumId); 440 updated |= artist.addAlbum(album); 441 //updated |= loadData(album); // TODO(b/123707561) Load separate from artist 442 } 443 } finally { 444 cursor.close(); 445 } 446 } 447 448 return updated; 449 } 450 loadAudios(@onNull Genre genre)451 boolean loadAudios(@NonNull Genre genre) { 452 boolean updated = false; 453 454 // TODO Remove hardcoded value 455 Uri contentUri = MediaStore.Audio.Genres.Members.getContentUri("external", genre.getId()); 456 String[] projection = { MediaStore.Audio.Genres.Members.AUDIO_ID }; 457 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); 458 if (cursor != null) { 459 try { 460 int audioIdColumn = cursor.getColumnIndexOrThrow( 461 MediaStore.Audio.Genres.Members.AUDIO_ID); 462 463 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 464 long audioId = cursor.getLong(audioIdColumn); 465 Audio audio = mMediaProvider.getAudioById(audioId); 466 updated |= genre.addAudio(audio); 467 updated |= loadData(audio); // TODO(b/123707561) Load separate from genre 468 } 469 } finally { 470 cursor.close(); 471 } 472 } 473 474 return updated; 475 } 476 loadAudios(@onNull Playlist playlist)477 boolean loadAudios(@NonNull Playlist playlist) { 478 boolean updated = false; 479 480 // TODO Remove hardcoded value 481 Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( 482 "external", playlist.getId()); 483 String[] projection = { MediaStore.Audio.Playlists.Members.AUDIO_ID }; 484 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); 485 if (cursor != null) { 486 try { 487 int audioIdColumn = cursor.getColumnIndexOrThrow( 488 MediaStore.Audio.Playlists.Members.AUDIO_ID); 489 490 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 491 long audioId = cursor.getLong(audioIdColumn); 492 Audio audio = mMediaProvider.getAudioById(audioId); 493 updated |= playlist.addAudio(audio); 494 updated |= loadData(audio); // TODO(b/123707561) Load separate from playlist 495 } 496 } finally { 497 cursor.close(); 498 } 499 } 500 501 return updated; 502 } 503 504 @Override onChange(boolean selfChange)505 public void onChange(boolean selfChange) { 506 Clog.i(TAG, "onChange(" + selfChange + ")"); 507 onChange(selfChange, null); 508 } 509 510 @Override onChange(boolean selfChange, @Nullable Uri uri)511 public void onChange(boolean selfChange, @Nullable Uri uri) { 512 Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")"); 513 // TODO(123705758) Figure out what changed 514 // onChange(false, content://media) 515 // onChange(false, content://media/external) 516 // onChange(false, content://media/external/audio/media/444) 517 // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0) 518 519 // TODO(123705758) Notify listener about changes 520 // mChangeListener.xxx(); 521 } 522 523 // TODO Remove unused methods createPlaylist(@onNull String name)524 private long createPlaylist(@NonNull String name) { 525 Clog.i(TAG, "createPlaylist(" + name + ")"); 526 Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; 527 ContentValues contentValues = new ContentValues(1); 528 contentValues.put(MediaStore.Audio.Playlists.NAME, name); 529 Uri uri = mContentResolver.insert(contentUri, contentValues); 530 return Long.parseLong(uri.getLastPathSegment()); 531 } 532 addToPlaylist(@onNull Playlist playlist, @NonNull Audio audio)533 private void addToPlaylist(@NonNull Playlist playlist, @NonNull Audio audio) { 534 Clog.i(TAG, "addToPlaylist(" + playlist + ", " + audio + ")"); 535 long base = getLastPlayOrder(playlist); 536 537 // TODO Remove hardcoded value 538 Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( 539 "external", playlist.getId()); 540 ContentValues contentValues = new ContentValues(2); 541 contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audio.getId()); 542 contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + 1); 543 mContentResolver.insert(contentUri, contentValues); 544 } 545 getLastPlayOrder(@onNull Playlist playlist)546 private long getLastPlayOrder(@NonNull Playlist playlist) { 547 Clog.i(TAG, "getLastPlayOrder(" + playlist + ")"); 548 549 long playOrder = -1; 550 551 // TODO Remove hardcoded value 552 Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( 553 "external", playlist.getId()); 554 String[] projection = { MediaStore.Audio.Playlists.Members.PLAY_ORDER }; 555 String sortOrder = MediaStore.Audio.Playlists.Members.PLAY_ORDER + " DESC LIMIT 1"; 556 Cursor cursor = mContentResolver.query( 557 contentUri, projection, null, null, sortOrder); 558 if (cursor != null) { 559 try { 560 int playOrderColumn = cursor.getColumnIndexOrThrow( 561 MediaStore.Audio.Playlists.Members.PLAY_ORDER); 562 563 if (cursor.moveToFirst()) { 564 playOrder = cursor.getLong(playOrderColumn); 565 } 566 } finally { 567 cursor.close(); 568 } 569 } 570 571 return playOrder; 572 } 573 } 574