blob: b0434af578b22f110024fe7014c2f2734a94ea69 [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.Manifest;
import android.content.ContentResolver;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
import androidx.collection.ArraySet;
import com.android.pump.concurrent.Executors;
import com.android.pump.util.Clog;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@UiThread
public class MediaDb implements MediaProvider {
private static final String TAG = Clog.tag(MediaDb.class);
private final AtomicBoolean mLoaded = new AtomicBoolean();
private final Executor mExecutor;
private final AudioStore mAudioStore;
private final VideoStore mVideoStore;
private final DataProvider mDataProvider;
private final List<Audio> mAudios = new ArrayList<>();
private final List<Artist> mArtists = new ArrayList<>();
private final List<Album> mAlbums = new ArrayList<>();
private final List<Genre> mGenres = new ArrayList<>();
private final List<Playlist> mPlaylists = new ArrayList<>();
private final List<Movie> mMovies = new ArrayList<>();
private final List<Series> mSeries = new ArrayList<>();
private final List<Episode> mEpisodes = new ArrayList<>();
private final List<Other> mOthers = new ArrayList<>();
private final Set<UpdateCallback> mAudioUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mArtistUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mAlbumUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mGenreUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mPlaylistUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mMovieUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mSeriesUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mEpisodeUpdateCallbacks = new ArraySet<>();
private final Set<UpdateCallback> mOtherUpdateCallbacks = new ArraySet<>();
public interface UpdateCallback {
void onItemsInserted(int index, int count);
void onItemsUpdated(int index, int count);
void onItemsRemoved(int index, int count);
}
public MediaDb(@NonNull ContentResolver contentResolver, @NonNull DataProvider dataProvider,
@NonNull Executor executor) {
Clog.i(TAG, "MediaDb(" + contentResolver + ", " + dataProvider + ", " + executor + ")");
mDataProvider = dataProvider;
mExecutor = executor;
mAudioStore = new AudioStore(contentResolver, new AudioStore.ChangeListener() {
@Override
public void onAudiosAdded(@NonNull Collection<Audio> audios) {
Executors.uiThreadExecutor().execute(() -> addAudios(audios));
}
@Override
public void onArtistsAdded(@NonNull Collection<Artist> artists) {
Executors.uiThreadExecutor().execute(() -> addArtists(artists));
}
@Override
public void onAlbumsAdded(@NonNull Collection<Album> albums) {
Executors.uiThreadExecutor().execute(() -> addAlbums(albums));
}
@Override
public void onGenresAdded(@NonNull Collection<Genre> genres) {
Executors.uiThreadExecutor().execute(() -> addGenres(genres));
}
@Override
public void onPlaylistsAdded(@NonNull Collection<Playlist> playlists) {
Executors.uiThreadExecutor().execute(() -> addPlaylists(playlists));
}
}, this);
mVideoStore = new VideoStore(contentResolver, new VideoStore.ChangeListener() {
@Override
public void onMoviesAdded(@NonNull Collection<Movie> movies) {
Executors.uiThreadExecutor().execute(() -> addMovies(movies));
}
@Override
public void onSeriesAdded(@NonNull Collection<Series> series) {
Executors.uiThreadExecutor().execute(() -> addSeries(series));
}
@Override
public void onEpisodesAdded(@NonNull Collection<Episode> episodes) {
Executors.uiThreadExecutor().execute(() -> addEpisodes(episodes));
}
@Override
public void onOthersAdded(@NonNull Collection<Other> others) {
Executors.uiThreadExecutor().execute(() -> addOthers(others));
}
}, this);
}
public void addAudioUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mAudioUpdateCallbacks, callback);
}
public void removeAudioUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mAudioUpdateCallbacks, callback);
}
public void addArtistUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mArtistUpdateCallbacks, callback);
}
public void removeArtistUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mArtistUpdateCallbacks, callback);
}
public void addAlbumUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mAlbumUpdateCallbacks, callback);
}
public void removeAlbumUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mAlbumUpdateCallbacks, callback);
}
public void addGenreUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mGenreUpdateCallbacks, callback);
}
public void removeGenreUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mGenreUpdateCallbacks, callback);
}
public void addPlaylistUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mPlaylistUpdateCallbacks, callback);
}
public void removePlaylistUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mPlaylistUpdateCallbacks, callback);
}
public void addMovieUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mMovieUpdateCallbacks, callback);
}
public void removeMovieUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mMovieUpdateCallbacks, callback);
}
public void addSeriesUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mSeriesUpdateCallbacks, callback);
}
public void removeSeriesUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mSeriesUpdateCallbacks, callback);
}
public void addEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mEpisodeUpdateCallbacks, callback);
}
public void removeEpisodeUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mEpisodeUpdateCallbacks, callback);
}
public void addOtherUpdateCallback(@NonNull UpdateCallback callback) {
addUpdateCallback(mOtherUpdateCallbacks, callback);
}
public void removeOtherUpdateCallback(@NonNull UpdateCallback callback) {
removeUpdateCallback(mOtherUpdateCallbacks, callback);
}
@RequiresPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
public void load() {
Clog.i(TAG, "load()");
if (mLoaded.getAndSet(true)) {
return;
}
mExecutor.execute(mAudioStore::load);
mExecutor.execute(mVideoStore::load);
}
public @NonNull List<Audio> getAudios() {
return Collections.unmodifiableList(mAudios);
}
public @NonNull List<Artist> getArtists() {
return Collections.unmodifiableList(mArtists);
}
public @NonNull List<Album> getAlbums() {
return Collections.unmodifiableList(mAlbums);
}
public @NonNull List<Genre> getGenres() {
return Collections.unmodifiableList(mGenres);
}
public @NonNull List<Playlist> getPlaylists() {
return Collections.unmodifiableList(mPlaylists);
}
public @NonNull List<Movie> getMovies() {
return Collections.unmodifiableList(mMovies);
}
public @NonNull List<Series> getSeries() {
return Collections.unmodifiableList(mSeries);
}
public @NonNull List<Episode> getEpisodes() {
return Collections.unmodifiableList(mEpisodes);
}
public @NonNull List<Other> getOthers() {
return Collections.unmodifiableList(mOthers);
}
public void loadData(@NonNull Audio audio) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (audio.isLoaded()) return;
mExecutor.execute(() -> {
boolean updated = mAudioStore.loadData(audio);
audio.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateAudio(audio));
}
});
}
public void loadData(@NonNull Artist artist) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (artist.isLoaded()) return;
mExecutor.execute(() -> {
try {
boolean updated = mDataProvider.populateArtist(artist);
updated |= mAudioStore.loadData(artist);
artist.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateArtist(artist));
}
} catch (IOException e) {
Clog.e(TAG, "Search for " + artist + " failed", e);
}
});
}
public void loadData(@NonNull Album album) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (album.isLoaded()) return;
mExecutor.execute(() -> {
try {
boolean updated = mDataProvider.populateAlbum(album);
updated |= mAudioStore.loadData(album);
album.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateAlbum(album));
}
} catch (IOException e) {
Clog.e(TAG, "Search for " + album + " failed", e);
}
});
}
public void loadData(@NonNull Genre genre) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (genre.isLoaded()) return;
mExecutor.execute(() -> {
boolean updated = mAudioStore.loadData(genre);
genre.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateGenre(genre));
}
});
}
public void loadData(@NonNull Playlist playlist) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (playlist.isLoaded()) return;
mExecutor.execute(() -> {
boolean updated = mAudioStore.loadData(playlist);
playlist.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updatePlaylist(playlist));
}
});
}
// TODO(b/123707018) Merge with loadData(episode)/loadData(other)
public void loadData(@NonNull Movie movie) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (movie.isLoaded()) return;
mExecutor.execute(() -> {
try {
boolean updated = mDataProvider.populateMovie(movie);
updated |= mVideoStore.loadData(movie);
movie.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateMovie(movie));
}
} catch (IOException e) {
Clog.e(TAG, "Search for " + movie + " failed", e);
}
});
}
public void loadData(@NonNull Series series) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (series.isLoaded()) return;
mExecutor.execute(() -> {
try {
boolean updated = mDataProvider.populateSeries(series);
updated |= mVideoStore.loadData(series);
series.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateSeries(series));
}
} catch (IOException e) {
Clog.e(TAG, "Search for " + series + " failed", e);
}
});
}
// TODO(b/123707018) Merge with loadData(movie)/loadData(other)
public void loadData(@NonNull Episode episode) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (episode.isLoaded()) return;
mExecutor.execute(() -> {
try {
boolean updated = mDataProvider.populateEpisode(episode);
updated |= mVideoStore.loadData(episode);
episode.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateEpisode(episode));
}
} catch (IOException e) {
Clog.e(TAG, "Search for " + episode + " failed", e);
}
});
}
// TODO(b/123707018) Merge with loadData(movie)/loadData(episode)
public void loadData(@NonNull Other other) {
// TODO(b/123707632) Ensure no concurrent runs for the same item !!
if (other.isLoaded()) return;
mExecutor.execute(() -> {
boolean updated = mVideoStore.loadData(other);
other.setLoaded();
if (updated) {
Executors.uiThreadExecutor().execute(() -> updateOther(other));
}
});
}
@Override
public @NonNull Audio getAudioById(long id) {
for (Audio audio : mAudios) {
if (audio.getId() == id) {
return audio;
}
}
throw new IllegalArgumentException("Audio with id " + id + " was not found");
}
@Override
public @NonNull Artist getArtistById(long id) {
for (Artist artist : mArtists) {
if (artist.getId() == id) {
return artist;
}
}
throw new IllegalArgumentException("Artist with id " + id + " was not found");
}
@Override
public @NonNull Album getAlbumById(long id) {
for (Album album : mAlbums) {
if (album.getId() == id) {
return album;
}
}
throw new IllegalArgumentException("Album with id " + id + " was not found");
}
@Override
public @NonNull Genre getGenreById(long id) {
for (Genre genre : mGenres) {
if (genre.getId() == id) {
return genre;
}
}
throw new IllegalArgumentException("Genre with id " + id + " was not found");
}
@Override
public @NonNull Playlist getPlaylistById(long id) {
for (Playlist playlist : mPlaylists) {
if (playlist.getId() == id) {
return playlist;
}
}
throw new IllegalArgumentException("Playlist with id " + id + " was not found");
}
@Override
public @NonNull Movie getMovieById(long id) {
for (Movie movie : mMovies) {
if (movie.getId() == id) {
return movie;
}
}
throw new IllegalArgumentException("Movie with id " + id + " was not found");
}
@Override
public @NonNull Series getSeriesById(@NonNull String title) {
for (Series series : mSeries) {
if (!series.hasYear() && series.getTitle().equals(title)) {
return series;
}
}
throw new IllegalArgumentException("Series '" + title + "' was not found");
}
@Override
public @NonNull Series getSeriesById(@NonNull String title, int year) {
for (Series series : mSeries) {
if (series.hasYear() && series.getTitle().equals(title) && series.getYear() == year) {
return series;
}
}
throw new IllegalArgumentException("Series '" + title + "' (" + year + ") was not found");
}
@Override
public @NonNull Episode getEpisodeById(long id) {
for (Episode episode : mEpisodes) {
if (episode.getId() == id) {
return episode;
}
}
throw new IllegalArgumentException("Episode with id " + id + " was not found");
}
@Override
public @NonNull Other getOtherById(long id) {
for (Other other : mOthers) {
if (other.getId() == id) {
return other;
}
}
throw new IllegalArgumentException("Other with id " + id + " was not found");
}
private void addUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
@NonNull UpdateCallback callback) {
if (!callbacks.add(callback)) {
throw new IllegalArgumentException("Callback " + callback + " already added in " +
callbacks);
}
}
private void removeUpdateCallback(@NonNull Set<UpdateCallback> callbacks,
@NonNull UpdateCallback callback) {
if (!callbacks.remove(callback)) {
throw new IllegalArgumentException("Callback " + callback + " not found in " +
callbacks);
}
}
private void addAudios(@NonNull Collection<Audio> audios) {
int audiosIndex = mAudios.size();
int audiosCount = 0;
mAudios.addAll(audios);
audiosCount += audios.size();
if (audiosCount > 0) {
for (UpdateCallback callback : mAudioUpdateCallbacks) {
callback.onItemsInserted(audiosIndex, audiosCount);
}
}
}
private void addArtists(@NonNull Collection<Artist> artists) {
int artistsIndex = mArtists.size();
int artistsCount = 0;
mArtists.addAll(artists);
artistsCount += artists.size();
if (artistsCount > 0) {
for (UpdateCallback callback : mArtistUpdateCallbacks) {
callback.onItemsInserted(artistsIndex, artistsCount);
}
}
}
private void addAlbums(@NonNull Collection<Album> albums) {
int albumsIndex = mAlbums.size();
int albumsCount = 0;
mAlbums.addAll(albums);
albumsCount += albums.size();
if (albumsCount > 0) {
for (UpdateCallback callback : mAlbumUpdateCallbacks) {
callback.onItemsInserted(albumsIndex, albumsCount);
}
}
}
private void addGenres(@NonNull Collection<Genre> genres) {
int genresIndex = mGenres.size();
int genresCount = 0;
mGenres.addAll(genres);
genresCount += genres.size();
if (genresCount > 0) {
for (UpdateCallback callback : mGenreUpdateCallbacks) {
callback.onItemsInserted(genresIndex, genresCount);
}
}
}
private void addPlaylists(@NonNull Collection<Playlist> playlists) {
int playlistsIndex = mPlaylists.size();
int playlistsCount = 0;
mPlaylists.addAll(playlists);
playlistsCount += playlists.size();
if (playlistsCount > 0) {
for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
callback.onItemsInserted(playlistsIndex, playlistsCount);
}
}
}
private void addMovies(@NonNull Collection<Movie> movies) {
int moviesIndex = mMovies.size();
int moviesCount = 0;
mMovies.addAll(movies);
moviesCount += movies.size();
if (moviesCount > 0) {
for (UpdateCallback callback : mMovieUpdateCallbacks) {
callback.onItemsInserted(moviesIndex, moviesCount);
}
}
}
private void addSeries(@NonNull Collection<Series> series) {
int seriesIndex = mSeries.size();
int seriesCount = 0;
mSeries.addAll(series);
seriesCount += series.size();
if (seriesCount > 0) {
for (UpdateCallback callback : mSeriesUpdateCallbacks) {
callback.onItemsInserted(seriesIndex, seriesCount);
}
}
}
private void addEpisodes(@NonNull Collection<Episode> episodes) {
int episodesIndex = mEpisodes.size();
int episodesCount = 0;
mEpisodes.addAll(episodes);
episodesCount += episodes.size();
if (episodesCount > 0) {
for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
callback.onItemsInserted(episodesIndex, episodesCount);
}
}
}
private void addOthers(@NonNull Collection<Other> others) {
int othersIndex = mOthers.size();
int othersCount = 0;
mOthers.addAll(others);
othersCount += others.size();
if (othersCount > 0) {
for (UpdateCallback callback : mOtherUpdateCallbacks) {
callback.onItemsInserted(othersIndex, othersCount);
}
}
}
private void updateAudio(@NonNull Audio audio) {
int index = mAudios.indexOf(audio);
if (index != -1) {
for (UpdateCallback callback : mAudioUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateArtist(@NonNull Artist artist) {
int index = mArtists.indexOf(artist);
if (index != -1) {
for (UpdateCallback callback : mArtistUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateAlbum(@NonNull Album album) {
int index = mAlbums.indexOf(album);
if (index != -1) {
for (UpdateCallback callback : mAlbumUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateGenre(@NonNull Genre genre) {
int index = mGenres.indexOf(genre);
if (index != -1) {
for (UpdateCallback callback : mGenreUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updatePlaylist(@NonNull Playlist playlist) {
int index = mPlaylists.indexOf(playlist);
if (index != -1) {
for (UpdateCallback callback : mPlaylistUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateMovie(@NonNull Movie movie) {
int index = mMovies.indexOf(movie);
if (index != -1) {
for (UpdateCallback callback : mMovieUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateSeries(@NonNull Series series) {
int index = mSeries.indexOf(series);
if (index != -1) {
for (UpdateCallback callback : mSeriesUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateEpisode(@NonNull Episode episode) {
int index = mEpisodes.indexOf(episode);
if (index != -1) {
for (UpdateCallback callback : mEpisodeUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
private void updateOther(@NonNull Other other) {
int index = mOthers.indexOf(other);
if (index != -1) {
for (UpdateCallback callback : mOtherUpdateCallbacks) {
callback.onItemsUpdated(index, 1);
}
}
}
}