blob: 9058a20f363d636a242eed11756d9eeb1a290303 [file] [log] [blame]
/*
* 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;
}
}