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