| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.pump.db; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.provider.MediaStore; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.WorkerThread; |
| |
| import com.android.pump.util.Clog; |
| import com.android.pump.util.Collections; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| |
| @WorkerThread |
| class AudioStore extends ContentObserver { |
| private static final String TAG = Clog.tag(AudioStore.class); |
| |
| private final ContentResolver mContentResolver; |
| private final ChangeListener mChangeListener; |
| private final MediaProvider mMediaProvider; |
| |
| interface ChangeListener { |
| void onAudiosAdded(@NonNull Collection<Audio> audios); |
| void onArtistsAdded(@NonNull Collection<Artist> artists); |
| void onAlbumsAdded(@NonNull Collection<Album> albums); |
| void onGenresAdded(@NonNull Collection<Genre> genres); |
| void onPlaylistsAdded(@NonNull Collection<Playlist> playlists); |
| } |
| |
| @AnyThread |
| AudioStore(@NonNull ContentResolver contentResolver, @NonNull ChangeListener changeListener, |
| @NonNull MediaProvider mediaProvider) { |
| super(null); |
| |
| Clog.i(TAG, "AudioStore(" + contentResolver + ", " + changeListener |
| + ", " + mediaProvider + ")"); |
| mContentResolver = contentResolver; |
| mChangeListener = changeListener; |
| mMediaProvider = mediaProvider; |
| |
| // TODO(123705758) Do we need content observer for other content uris? (E.g. album, artist) |
| mContentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, |
| true, this); |
| |
| // TODO(123705758) When to call unregisterContentObserver? |
| // mContentResolver.unregisterContentObserver(this); |
| } |
| |
| void load() { |
| Clog.i(TAG, "load()"); |
| ArrayList<Artist> artists = new ArrayList<>(); |
| ArrayList<Album> albums = new ArrayList<>(); |
| ArrayList<Audio> audios = new ArrayList<>(); |
| ArrayList<Playlist> playlists = new ArrayList<>(); |
| ArrayList<Genre> genres = new ArrayList<>(); |
| |
| // #1 Load artists |
| { |
| Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Artists._ID |
| }; |
| String sortOrder = MediaStore.Audio.Artists._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| |
| Artist artist = new Artist(id); |
| artists.add(artist); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| // #2 Load albums and connect each to artist |
| { |
| Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Albums._ID, |
| MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID |
| }; |
| String sortOrder = MediaStore.Audio.Albums._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID); |
| int artistIdColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| |
| Album album = new Album(id); |
| albums.add(album); |
| |
| if (!cursor.isNull(artistIdColumn)) { |
| long artistId = cursor.getLong(artistIdColumn); |
| |
| Artist artist = Collections.find(artists, artistId, Artist::getId); |
| album.setArtist(artist); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| // #3 Load songs and connect each to album and artist |
| { |
| Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Media._ID, |
| MediaStore.Audio.Media.MIME_TYPE, |
| MediaStore.Audio.Media.ARTIST_ID, |
| MediaStore.Audio.Media.ALBUM_ID |
| }; |
| String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"; |
| String sortOrder = MediaStore.Audio.Media._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, selection, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); |
| int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE); |
| int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); |
| int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| String mimeType = cursor.getString(mimeTypeColumn); |
| |
| Audio audio = new Audio(id, mimeType); |
| audios.add(audio); |
| |
| if (!cursor.isNull(artistIdColumn)) { |
| long artistId = cursor.getLong(artistIdColumn); |
| |
| Artist artist = Collections.find(artists, artistId, Artist::getId); |
| audio.setArtist(artist); |
| artist.addAudio(audio); |
| } |
| if (!cursor.isNull(albumIdColumn)) { |
| long albumId = cursor.getLong(albumIdColumn); |
| |
| Album album = Collections.find(albums, albumId, Album::getId); |
| audio.setAlbum(album); |
| album.addAudio(audio); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| // #4 Load playlists (optional?) |
| { |
| Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Playlists._ID |
| }; |
| String sortOrder = MediaStore.Audio.Playlists._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| |
| Playlist playlist = new Playlist(id); |
| playlists.add(playlist); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| // #5 Load genres (optional?) |
| { |
| Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Genres._ID |
| }; |
| String sortOrder = MediaStore.Audio.Genres._ID; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long id = cursor.getLong(idColumn); |
| |
| Genre genre = new Genre(id); |
| genres.add(genre); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| } |
| |
| mChangeListener.onAudiosAdded(audios); |
| mChangeListener.onArtistsAdded(artists); |
| mChangeListener.onAlbumsAdded(albums); |
| mChangeListener.onGenresAdded(genres); |
| mChangeListener.onPlaylistsAdded(playlists); |
| } |
| |
| boolean loadData(@NonNull Audio audio) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Media.TITLE, |
| MediaStore.Audio.Media.ARTIST_ID, |
| MediaStore.Audio.Media.ALBUM_ID |
| }; |
| String selection = MediaStore.Audio.Media._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(audio.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); |
| int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); |
| int albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID); |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(titleColumn)) { |
| String title = cursor.getString(titleColumn); |
| updated |= audio.setTitle(title); |
| } |
| if (!cursor.isNull(artistIdColumn)) { |
| long artistId = cursor.getLong(artistIdColumn); |
| Artist artist = mMediaProvider.getArtistById(artistId); |
| updated |= audio.setArtist(artist); |
| updated |= loadData(artist); // TODO(b/123707561) Load separate from audio |
| } |
| if (!cursor.isNull(albumIdColumn)) { |
| long albumId = cursor.getLong(albumIdColumn); |
| Album album = mMediaProvider.getAlbumById(albumId); |
| updated |= audio.setAlbum(album); |
| updated |= loadData(album); // TODO(b/123707561) Load separate from audio |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return updated; |
| } |
| |
| boolean loadData(@NonNull Artist artist) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; |
| String[] projection = { MediaStore.Audio.Artists.ARTIST }; |
| String selection = MediaStore.Audio.Artists._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(artist.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST); |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(artistColumn)) { |
| String name = cursor.getString(artistColumn); |
| updated |= artist.setName(name); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| updated |= loadAlbums(artist); // TODO(b/123707561) Load separate from artist |
| |
| return updated; |
| } |
| |
| boolean loadData(@NonNull Album album) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; |
| String[] projection = { |
| MediaStore.Audio.Albums.ALBUM, |
| MediaStore.Audio.Media.ARTIST_ID // TODO MediaStore.Audio.Albums.ARTIST_ID |
| }; |
| String selection = MediaStore.Audio.Albums._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(album.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM); |
| int artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); // TODO MediaStore.Audio.Albums.ARTIST_ID |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(albumColumn)) { |
| String albumTitle = cursor.getString(albumColumn); |
| updated |= album.setTitle(albumTitle); |
| } |
| if (!cursor.isNull(artistIdColumn)) { |
| long artistId = cursor.getLong(artistIdColumn); |
| Artist artist = mMediaProvider.getArtistById(artistId); |
| updated |= album.setArtist(artist); |
| updated |= loadData(artist); // TODO(b/123707561) Load separate from album |
| } |
| |
| // TODO(b/130363861) No need to store the URI -- generate when requested instead |
| Uri albumArtUri = new Uri.Builder() |
| .scheme(ContentResolver.SCHEME_CONTENT) |
| .authority(MediaStore.AUTHORITY) |
| .appendPath("external").appendPath("audio").appendPath("albumart") |
| .appendPath(Long.toString(album.getId())).build(); |
| updated |= album.setAlbumArtUri(albumArtUri); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return updated; |
| } |
| |
| boolean loadData(@NonNull Genre genre) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI; |
| String[] projection = { MediaStore.Audio.Genres.NAME }; |
| String selection = MediaStore.Audio.Genres._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(genre.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME); |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(nameColumn)) { |
| String name = cursor.getString(nameColumn); |
| updated |= genre.setName(name); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| updated |= loadAudios(genre); // TODO(b/123707561) Load separate from genre |
| |
| return updated; |
| } |
| |
| boolean loadData(@NonNull Playlist playlist) { |
| boolean updated = false; |
| |
| Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; |
| String[] projection = { MediaStore.Audio.Playlists.NAME }; |
| String selection = MediaStore.Audio.Playlists._ID + " = ?"; |
| String[] selectionArgs = { Long.toString(playlist.getId()) }; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, selection, selectionArgs, null); |
| if (cursor != null) { |
| try { |
| int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME); |
| |
| if (cursor.moveToFirst()) { |
| if (!cursor.isNull(nameColumn)) { |
| String name = cursor.getString(nameColumn); |
| updated |= playlist.setName(name); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| updated |= loadAudios(playlist); // TODO(b/123707561) Load separate from playlist |
| |
| return updated; |
| } |
| |
| boolean loadAlbums(@NonNull Artist artist) { |
| boolean updated = false; |
| |
| // TODO Remove hardcoded value |
| Uri contentUri = MediaStore.Audio.Artists.Albums.getContentUri("external", artist.getId()); |
| /* |
| * On some devices MediaStore doesn't use ALBUM_ID as key from Artist to Album, but rather |
| * _ID. In order to support these devices we don't pass a projection, to avoid the |
| * IllegalArgumentException(Invalid column) exception, and then resort to _ID. |
| */ |
| String[] projection = null; // { MediaStore.Audio.Artists.Albums.ALBUM_ID }; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); |
| if (cursor != null) { |
| try { |
| int albumIdColumn = cursor.getColumnIndex(MediaStore.Audio.Artists.Albums.ALBUM_ID); |
| if (albumIdColumn < 0) { |
| // On some devices the ALBUM_ID column doesn't exist and _ID is used instead. |
| albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); |
| } |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long albumId = cursor.getLong(albumIdColumn); |
| Album album = mMediaProvider.getAlbumById(albumId); |
| updated |= artist.addAlbum(album); |
| //updated |= loadData(album); // TODO(b/123707561) Load separate from artist |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return updated; |
| } |
| |
| boolean loadAudios(@NonNull Genre genre) { |
| boolean updated = false; |
| |
| // TODO Remove hardcoded value |
| Uri contentUri = MediaStore.Audio.Genres.Members.getContentUri("external", genre.getId()); |
| String[] projection = { MediaStore.Audio.Genres.Members.AUDIO_ID }; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); |
| if (cursor != null) { |
| try { |
| int audioIdColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Audio.Genres.Members.AUDIO_ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long audioId = cursor.getLong(audioIdColumn); |
| Audio audio = mMediaProvider.getAudioById(audioId); |
| updated |= genre.addAudio(audio); |
| updated |= loadData(audio); // TODO(b/123707561) Load separate from genre |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return updated; |
| } |
| |
| boolean loadAudios(@NonNull Playlist playlist) { |
| boolean updated = false; |
| |
| // TODO Remove hardcoded value |
| Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( |
| "external", playlist.getId()); |
| String[] projection = { MediaStore.Audio.Playlists.Members.AUDIO_ID }; |
| Cursor cursor = mContentResolver.query(contentUri, projection, null, null, null); |
| if (cursor != null) { |
| try { |
| int audioIdColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Audio.Playlists.Members.AUDIO_ID); |
| |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| long audioId = cursor.getLong(audioIdColumn); |
| Audio audio = mMediaProvider.getAudioById(audioId); |
| updated |= playlist.addAudio(audio); |
| updated |= loadData(audio); // TODO(b/123707561) Load separate from playlist |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return updated; |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| Clog.i(TAG, "onChange(" + selfChange + ")"); |
| onChange(selfChange, null); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, @Nullable Uri uri) { |
| Clog.i(TAG, "onChange(" + selfChange + ", " + uri + ")"); |
| // TODO(123705758) Figure out what changed |
| // onChange(false, content://media) |
| // onChange(false, content://media/external) |
| // onChange(false, content://media/external/audio/media/444) |
| // onChange(false, content://media/external/video/media/328?blocking=1&orig_id=328&group_id=0) |
| |
| // TODO(123705758) Notify listener about changes |
| // mChangeListener.xxx(); |
| } |
| |
| // TODO Remove unused methods |
| private long createPlaylist(@NonNull String name) { |
| Clog.i(TAG, "createPlaylist(" + name + ")"); |
| Uri contentUri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; |
| ContentValues contentValues = new ContentValues(1); |
| contentValues.put(MediaStore.Audio.Playlists.NAME, name); |
| Uri uri = mContentResolver.insert(contentUri, contentValues); |
| return Long.parseLong(uri.getLastPathSegment()); |
| } |
| |
| private void addToPlaylist(@NonNull Playlist playlist, @NonNull Audio audio) { |
| Clog.i(TAG, "addToPlaylist(" + playlist + ", " + audio + ")"); |
| long base = getLastPlayOrder(playlist); |
| |
| // TODO Remove hardcoded value |
| Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( |
| "external", playlist.getId()); |
| ContentValues contentValues = new ContentValues(2); |
| contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audio.getId()); |
| contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, base + 1); |
| mContentResolver.insert(contentUri, contentValues); |
| } |
| |
| private long getLastPlayOrder(@NonNull Playlist playlist) { |
| Clog.i(TAG, "getLastPlayOrder(" + playlist + ")"); |
| |
| long playOrder = -1; |
| |
| // TODO Remove hardcoded value |
| Uri contentUri = MediaStore.Audio.Playlists.Members.getContentUri( |
| "external", playlist.getId()); |
| String[] projection = { MediaStore.Audio.Playlists.Members.PLAY_ORDER }; |
| String sortOrder = MediaStore.Audio.Playlists.Members.PLAY_ORDER + " DESC LIMIT 1"; |
| Cursor cursor = mContentResolver.query( |
| contentUri, projection, null, null, sortOrder); |
| if (cursor != null) { |
| try { |
| int playOrderColumn = cursor.getColumnIndexOrThrow( |
| MediaStore.Audio.Playlists.Members.PLAY_ORDER); |
| |
| if (cursor.moveToFirst()) { |
| playOrder = cursor.getLong(playOrderColumn); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| return playOrder; |
| } |
| } |