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.io.File; 35 import java.util.ArrayList; 36 import java.util.Collection; 37 38 @WorkerThread 39 class AudioStore extends ContentObserver { 40 private static final String TAG = Clog.tag(AudioStore.class); 41 42 private final ContentResolver mContentResolver; 43 private final ChangeListener mChangeListener; 44 private final MediaProvider mMediaProvider; 45 46 interface ChangeListener { onAudiosAdded(@onNull Collection<Audio> audios)47 void onAudiosAdded(@NonNull Collection<Audio> audios); onArtistsAdded(@onNull Collection<Artist> artists)48 void onArtistsAdded(@NonNull Collection<Artist> artists); onAlbumsAdded(@onNull Collection<Album> albums)49 void onAlbumsAdded(@NonNull Collection<Album> albums); onGenresAdded(@onNull Collection<Genre> genres)50 void onGenresAdded(@NonNull Collection<Genre> genres); onPlaylistsAdded(@onNull Collection<Playlist> playlists)51 void onPlaylistsAdded(@NonNull Collection<Playlist> playlists); 52 } 53 54 @AnyThread AudioStore(@onNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, @NonNull MediaProvider mediaProvider)55 AudioStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, 56 @NonNull MediaProvider mediaProvider) { 57 super(null); 58 59 Clog.i(TAG, "AudioStore(" + contentResolver + ", " + changeListener 60 + ", " + mediaProvider + ")"); 61 mContentResolver = contentResolver; 62 mChangeListener = changeListener; 63 mMediaProvider = mediaProvider; 64 65 // TODO(123705758) Do we need content observer for other content uris? (E.g. album, artist) 66 mContentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 67 true, this); 68 69 // TODO(123705758) When to call unregisterContentObserver? 70 // mContentResolver.unregisterContentObserver(this); 71 } 72 load()73 void load() { 74 Clog.i(TAG, "load()"); 75 ArrayList<Artist> artists = new ArrayList<>(); 76 ArrayList<Album> albums = new ArrayList<>(); 77 ArrayList<Audio> audios = new ArrayList<>(); 78 ArrayList<Playlist> playlists = new ArrayList<>(); 79 ArrayList<Genre> genres = new ArrayList<>(); 80 81 // #1 Load artists 82 { 83 Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; 84 String[] projection = { 85 MediaStore.Audio.Artists._ID 86 }; 87 String sortOrder = MediaStore.Audio.Artists._ID; 88 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 89 if (cursor != null) { 90 try { 91 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID); 92 93 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 94 long id = cursor.getLong(idColumn); 95 96 Artist artist = new Artist(id); 97 artists.add(artist); 98 } 99 } finally { 100 cursor.close(); 101 } 102 } 103 } 104 105 // #2 Load albums and connect each to artist 106 { 107 Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; 108 String[] projection = { 109 MediaStore.Audio.Albums._ID, 110 MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID 111 }; 112 String sortOrder = MediaStore.Audio.Albums._ID; 113 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 114 if (cursor != null) { 115 try { 116 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID); 117 int artistIdColumn = cursor.getColumnIndexOrThrow( 118 MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID 119 120 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 121 long id = cursor.getLong(idColumn); 122 123 Album album = new Album(id); 124 albums.add(album); 125 126 if (!cursor.isNull(artistIdColumn)) { 127 long artistId = cursor.getLong(artistIdColumn); 128 129 Artist artist = Collections.find(artists, artistId, Artist::getId); 130 album.setArtist(artist); 131 } 132 } 133 } finally { 134 cursor.close(); 135 } 136 } 137 } 138 139 // #3 Load songs and connect each to album and artist 140 { 141 Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 142 String[] projection = { 143 MediaStore.Audio.Media._ID, 144 MediaStore.Audio.Media.MIME_TYPE, 145 MediaStore.Audio.Media.ARTIST_ID, 146 MediaStore.Audio.Media.ALBUM_ID 147 }; 148 String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"; 149 String sortOrder = MediaStore.Audio.Media._ID; 150 Cursor cursor = mContentResolver.query(contentUri, projection, selection, null, sortOrder); 151 if (cursor != null) { 152 try { 153 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 154 int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE); 155 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); 156 int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); 157 158 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 159 long id = cursor.getLong(idColumn); 160 String mimeType = cursor.getString(mimeTypeColumn); 161 162 Audio audio = new Audio(id, mimeType); 163 audios.add(audio); 164 165 if (!cursor.isNull(artistIdColumn)) { 166 long artistId = cursor.getLong(artistIdColumn); 167 168 Artist artist = Collections.find(artists, artistId, Artist::getId); 169 audio.setArtist(artist); 170 artist.addAudio(audio); 171 } 172 if (!cursor.isNull(albumIdColumn)) { 173 long albumId = cursor.getLong(albumIdColumn); 174 175 Album album = Collections.find(albums, albumId, Album::getId); 176 audio.setAlbum(album); 177 album.addAudio(audio); 178 } 179 } 180 } finally { 181 cursor.close(); 182 } 183 } 184 } 185 186 // #4 Load playlists (optional?) 187 { 188 Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; 189 String[] projection = { 190 MediaStore.Audio.Playlists._ID 191 }; 192 String sortOrder = MediaStore.Audio.Playlists._ID; 193 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 194 if (cursor != null) { 195 try { 196 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID); 197 198 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 199 long id = cursor.getLong(idColumn); 200 201 Playlist playlist = new Playlist(id); 202 playlists.add(playlist); 203 } 204 } finally { 205 cursor.close(); 206 } 207 } 208 } 209 210 // #5 Load genres (optional?) 211 { 212 Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; 213 String[] projection = { 214 MediaStore.Audio.Genres._ID 215 }; 216 String sortOrder = MediaStore.Audio.Genres._ID; 217 Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); 218 if (cursor != null) { 219 try { 220 int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID); 221 222 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 223 long id = cursor.getLong(idColumn); 224 225 Genre genre = new Genre(id); 226 genres.add(genre); 227 } 228 } finally { 229 cursor.close(); 230 } 231 } 232 } 233 234 mChangeListener.onAudiosAdded(audios); 235 mChangeListener.onArtistsAdded(artists); 236 mChangeListener.onAlbumsAdded(albums); 237 mChangeListener.onGenresAdded(genres); 238 mChangeListener.onPlaylistsAdded(playlists); 239 } 240 loadData(@onNull Audio audio)241 boolean loadData(@NonNull Audio audio) { 242 boolean updated = false; 243 244 Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 245 String[] projection = { 246 MediaStore.Audio.Media.TITLE, 247 MediaStore.Audio.Media.ARTIST_ID, 248 MediaStore.Audio.Media.ALBUM_ID 249 }; 250 String selection = MediaStore.Audio.Media._ID + " = ?"; 251 String[] selectionArgs = { Long.toString(audio.getId()) }; 252 Cursor cursor = mContentResolver.query( 253 contentUri, projection, selection, selectionArgs, null); 254 if (cursor != null) { 255 try { 256 int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); 257 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); 258 int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); 259 260 if (cursor.moveToFirst()) { 261 if (!cursor.isNull(titleColumn)) { 262 String title = cursor.getString(titleColumn); 263 updated |= audio.setTitle(title); 264 } 265 if (!cursor.isNull(artistIdColumn)) { 266 long artistId = cursor.getLong(artistIdColumn); 267 Artist artist = mMediaProvider.getArtistById(artistId); 268 updated |= audio.setArtist(artist); 269 updated |= loadData(artist); // TODO(b/123707561) Load separate from audio 270 } 271 if (!cursor.isNull(albumIdColumn)) { 272 long albumId = cursor.getLong(albumIdColumn); 273 Album album = mMediaProvider.getAlbumById(albumId); 274 updated |= audio.setAlbum(album); 275 updated |= loadData(album); // TODO(b/123707561) Load separate from audio 276 } 277 } 278 } finally { 279 cursor.close(); 280 } 281 } 282 283 return updated; 284 } 285 loadData(@onNull Artist artist)286 boolean loadData(@NonNull Artist artist) { 287 boolean updated = false; 288 289 Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; 290 String[] projection = { MediaStore.Audio.Artists.ARTIST }; 291 String selection = MediaStore.Audio.Artists._ID + " = ?"; 292 String[] selectionArgs = { Long.toString(artist.getId()) }; 293 Cursor cursor = mContentResolver.query( 294 contentUri, projection, selection, selectionArgs, null); 295 if (cursor != null) { 296 try { 297 int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST); 298 299 if (cursor.moveToFirst()) { 300 if (!cursor.isNull(artistColumn)) { 301 String name = cursor.getString(artistColumn); 302 updated |= artist.setName(name); 303 } 304 } 305 } finally { 306 cursor.close(); 307 } 308 } 309 310 updated |= loadAlbums(artist); // TODO(b/123707561) Load separate from artist 311 312 return updated; 313 } 314 loadData(@onNull Album album)315 boolean loadData(@NonNull Album album) { 316 boolean updated = false; 317 318 Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; 319 String[] projection = { 320 MediaStore.Audio.Albums.ALBUM_ART, 321 MediaStore.Audio.Albums.ALBUM, 322 MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID 323 }; 324 String selection = MediaStore.Audio.Albums._ID + " = ?"; 325 String[] selectionArgs = { Long.toString(album.getId()) }; 326 Cursor cursor = mContentResolver.query( 327 contentUri, projection, selection, selectionArgs, null); 328 if (cursor != null) { 329 try { 330 int albumArtColumn = cursor.getColumnIndexOrThrow( 331 MediaStore.Audio.Albums.ALBUM_ART); 332 int albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM); 333 int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID 334 335 if (cursor.moveToFirst()) { 336 if (!cursor.isNull(albumColumn)) { 337 String albumTitle = cursor.getString(albumColumn); 338 updated |= album.setTitle(albumTitle); 339 } 340 if (!cursor.isNull(albumArtColumn)) { 341 Uri albumArtUri = Uri.fromFile(new File(cursor.getString(albumArtColumn))); 342 updated |= album.setAlbumArtUri(albumArtUri); 343 } 344 if (!cursor.isNull(artistIdColumn)) { 345 long artistId = cursor.getLong(artistIdColumn); 346 Artist artist = mMediaProvider.getArtistById(artistId); 347 updated |= album.setArtist(artist); 348 updated |= loadData(artist); // TODO(b/123707561) Load separate from album 349 } 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