package com.simplecity.amp_library.utils;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.crashlytics.android.Crashlytics;
import com.jakewharton.rxrelay.BehaviorRelay;
import com.simplecity.amp_library.ShuttleApplication;
import com.simplecity.amp_library.model.Album;
import com.simplecity.amp_library.model.AlbumArtist;
import com.simplecity.amp_library.model.BlacklistedSong;
import com.simplecity.amp_library.model.Genre;
import com.simplecity.amp_library.model.Playlist;
import com.simplecity.amp_library.model.Song;
import com.simplecity.amp_library.model.WhitelistFolder;
import com.simplecity.amp_library.sql.databases.BlacklistDbOpenHelper;
import com.simplecity.amp_library.sql.databases.WhitelistDbOpenHelper;
import com.simplecity.amp_library.sql.sqlbrite.SqlBriteUtils;
import com.squareup.sqlbrite.BriteDatabase;
import com.squareup.sqlbrite.SqlBrite;
import java.util.ArrayList;
import java.util.List;
import rx.Observable;
import rx.Subscription;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
public class DataManager {
private static final String TAG = "DataManager";
private static DataManager instance;
private Subscription songsSubscription;
private BehaviorRelay<List<Song>> songsRelay = BehaviorRelay.create();
private Subscription albumsSubscription;
private BehaviorRelay<List<Album>> albumsRelay = BehaviorRelay.create();
private Subscription albumArtistsSubscription;
private BehaviorRelay<List<AlbumArtist>> albumArtistsRelay = BehaviorRelay.create();
private Subscription genresSubscription;
private BehaviorRelay<List<Genre>> genresRelay = BehaviorRelay.create();
private Subscription playlistsSubscription;
private BehaviorRelay<List<Playlist>> playlistsRelay = BehaviorRelay.create();
private BriteDatabase blacklistDatabase;
private Subscription blacklistSubscription;
private BehaviorRelay<List<BlacklistedSong>> blacklistRelay = BehaviorRelay.create();
private BriteDatabase whitelistDatabase;
private Subscription whitelistSubscription;
private BehaviorRelay<List<WhitelistFolder>> whitelistRelay = BehaviorRelay.create();
public static DataManager getInstance() {
if (instance == null) {
instance = new DataManager();
}
return instance;
}
private DataManager() {
}
/**
* Returns an {@link Observable}, which emits a List of {@link Song}s retrieved from the MediaStore.
* <p>
* This Observable is continuous. It will emit its most recent value upon subscription, and continue to emit
* whenever the underlying {@code uri}'s data changes.
* <p>
* This Observable is backed by an {@link BehaviorRelay} subscribed to a {@link SqlBrite} {@link Observable}.
* <p>
* <b>Caution:</b>
* <p>
* Although the underlying {@link SqlBrite} {@link Observable} is subscribed on the {@link Schedulers#io()} thread,
* it seems the {@code Scheduler} will not persist for subsequent {@code subscribe} calls once this {@link Observable} is unsubscribed.
* Presumably, since subsequent {@code subscribe} calls to this {@link Observable} only re-emit the most recent emission from the
* source {@link Observable}, the source {@link Observable} is no longer part of the current Observable chain (its job is now
* just to keep the {@link BehaviorRelay} up to date). So a {@code Scheduler} must be supplied if you wish to ensure the work of this
* {@link Observable} is not done on the calling thread. For now, the {@link Observable} is automatically subscribed on the
* {@link Schedulers#io()} {@code scheduler}.
*/
public Observable<List<Song>> getSongsRelay() {
if (songsSubscription == null || songsSubscription.isUnsubscribed()) {
Observable<List<Song>> songsObservable = SqlBriteUtils.createContinuousQuery(ShuttleApplication.getInstance(), Song::new, Song.getQuery());
songsSubscription = Observable.combineLatest(songsObservable, getBlacklistRelay(), getWhitelistRelay(), (songs, blacklistedSongs, whitelistFolders) ->
{
List<Song> result = songs;
//Filter out blacklisted songs
if (!blacklistedSongs.isEmpty()) {
result = Stream.of(songs)
.filter(song -> !Stream.of(blacklistedSongs)
.anyMatch(blacklistedSong -> blacklistedSong.songId == song.id))
.collect(Collectors.toList());
}
//Filter out non-whitelisted folders
if (!whitelistFolders.isEmpty()) {
result = Stream.of(result)
.filter(song -> Stream.of(whitelistFolders)
.anyMatch(whitelistFolder -> StringUtils.containsIgnoreCase(song.path, whitelistFolder.folder)))
.collect(Collectors.toList());
}
return result;
}).subscribe(songsRelay, error -> Crashlytics.log("getSongsRelay error: " + error.getMessage()));
}
return songsRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
/**
* Returns an {@link Observable}, which emits a List of {@link Album}s built from the {@link Song}s returned by
* {@link #getSongsRelay()}.
* <p>
* This Observable is continuous. It will emit its most recent value upon subscription, and continue to emit
* whenever the underlying {@code uri}'s data changes.
* <p>
* This Observable is backed by an {@link BehaviorRelay} subscribed to a {@link SqlBrite} {@link Observable}.
* <p>
* <b>Caution:</b>
* <p>
* Although the underlying {@link SqlBrite} {@link Observable} is subscribed on the {@link Schedulers#io()} thread,
* it seems the {@code Scheduler} will not persist for subsequent {@code subscribe} calls once this {@link Observable} is unsubscribed.
* Presumably, since subsequent {@code subscribe} calls to this {@link Observable} only re-emit the most recent emission from the
* source {@link Observable}, the source {@link Observable} is no longer part of the current Observable chain (its job is now
* just to keep the {@link BehaviorRelay} up to date). So a {@code Scheduler} must be supplied if you wish to ensure the work of this
* {@link Observable} is not done on the calling thread. For now, the {@link Observable} is automatically subscribed on the
* {@link Schedulers#io()} {@code scheduler}.
*/
public Observable<List<Album>> getAlbumsRelay() {
if (albumsSubscription == null || albumsSubscription.isUnsubscribed()) {
albumsSubscription = getSongsRelay()
.flatMap(songs -> Observable.just(Operators.songsToAlbums(songs)))
.subscribe(albumsRelay, error -> Crashlytics.log("getAlbumsRelay error: " + error.getMessage()));
}
return albumsRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
/**
* Returns an {@link Observable}, which emits a List of {@link AlbumArtist}s built from the {@link Album}s returned by
* {@link #getAlbumsRelay()}.
* <p>
* This Observable is continuous. It will emit its most recent value upon subscription, and continue to emit
* whenever the underlying {@code uri}'s data changes.
* <p>
* This Observable is backed by an {@link BehaviorRelay} subscribed to a {@link SqlBrite} {@link Observable}.
* <p>
* <b>Caution:</b>
* <p>
* Although the underlying {@link SqlBrite} {@link Observable} is subscribed on the {@link Schedulers#io()} thread,
* it seems the {@code Scheduler} will not persist for subsequent {@code subscribe} calls once this {@link Observable} is unsubscribed.
* Presumably, since subsequent {@code subscribe} calls to this {@link Observable} only re-emit the most recent emission from the
* source {@link Observable}, the source {@link Observable} is no longer part of the current Observable chain (its job is now
* just to keep the {@link BehaviorRelay} up to date). So a {@code Scheduler} must be supplied if you wish to ensure the work of this
* {@link Observable} is not done on the calling thread. For now, the {@link Observable} is automatically subscribed on the
* {@link Schedulers#io()} {@code scheduler}.
*/
public Observable<List<AlbumArtist>> getAlbumArtistsRelay() {
if (albumArtistsSubscription == null || albumArtistsSubscription.isUnsubscribed()) {
albumArtistsSubscription = getAlbumsRelay()
.flatMap(albums -> Observable.just(Operators.albumsToAlbumArtists(albums)))
.subscribe(albumArtistsRelay, error -> Crashlytics.log("getAlbumArtistsRelay error: " + error.getMessage()));
}
return albumArtistsRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
/**
* Returns an {@link Observable}, which emits a List of {@link Genre}s retrieved from the MediaStore.
* <p>
* This Observable is continuous. It will emit its most recent value upon subscription, and continue to emit
* whenever the underlying {@code uri}'s data changes.
* <p>
* This Observable is backed by an {@link BehaviorRelay} subscribed to a {@link SqlBrite} {@link Observable}.
* <p>
* <b>Caution:</b>
* <p>
* Although the underlying {@link SqlBrite} {@link Observable} is subscribed on the {@link Schedulers#io()} thread,
* it seems the {@code Scheduler} will not persist for subsequent {@code subscribe} calls once this {@link Observable} is unsubscribed.
* Presumably, since subsequent {@code subscribe} calls to this {@link Observable} only re-emit the most recent emission from the
* source {@link Observable}, the source {@link Observable} is no longer part of the current Observable chain (its job is now
* just to keep the {@link BehaviorRelay} up to date). So a {@code Scheduler} must be supplied if you wish to ensure the work of this
* {@link Observable} is not done on the calling thread. For now, the {@link Observable} is automatically subscribed on the
* {@link Schedulers#io()} {@code scheduler}.
*/
public Observable<List<Genre>> getGenresRelay() {
if (genresSubscription == null || genresSubscription.isUnsubscribed()) {
genresSubscription = SqlBriteUtils.createContinuousQuery(ShuttleApplication.getInstance(), Genre::new, Genre.getQuery())
.subscribe(genresRelay, error -> Crashlytics.log("getGenresRelay error: " + error.getMessage()));
}
return genresRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
public void updateGenresRelay(List<Genre> genres){
genresRelay.call(genres);
}
/**
* Returns an {@link Observable}, which emits a List of {@link Playlist}s retrieved from the MediaStore.
* <p>
* This Observable is continuous. It will emit its most recent value upon subscription, and continue to emit
* whenever the underlying {@code uri}'s data changes.
* <p>
* This Observable is backed by an {@link BehaviorRelay} subscribed to a {@link SqlBrite} {@link Observable}.
* <p>
* <b>Caution:</b>
* <p>
* Although the underlying {@link SqlBrite} {@link Observable} is subscribed on the {@link Schedulers#io()} thread,
* it seems the {@code Scheduler} will not persist for subsequent {@code subscribe} calls once this {@link Observable} is unsubscribed.
* Presumably, since subsequent {@code subscribe} calls to this {@link Observable} only re-emit the most recent emission from the
* source {@link Observable}, the source {@link Observable} is no longer part of the current Observable chain (its job is now
* just to keep the {@link BehaviorRelay} up to date). So a {@code Scheduler} must be supplied if you wish to ensure the work of this
* {@link Observable} is not done on the calling thread. For now, the {@link Observable} is automatically subscribed on the
* {@link Schedulers#io()} {@code scheduler}.
*/
public Observable<List<Playlist>> getPlaylistsRelay() {
if (playlistsSubscription == null || playlistsSubscription.isUnsubscribed()) {
playlistsSubscription = SqlBriteUtils.createContinuousQuery(ShuttleApplication.getInstance(), Playlist::new, Playlist.getQuery())
.subscribe(playlistsRelay, error -> Crashlytics.log("getPlaylistRelay error: " + error.getMessage()));
}
return playlistsRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
/**
* Returns an Observable<List<Song>> from the songs relay, filtered by the passed in predicate.
* <p>
* This Observable is finite (it only emits once),
*/
public Observable<List<Song>> getSongsObservable(Func1<Song, Boolean> predicate) {
return getSongsRelay()
.first()
.flatMap(Observable::from)
.filter(predicate).toList();
}
/**
* @return a {@link BriteDatabase} wrapping the blacklist SqliteOpenHelper.
*/
public BriteDatabase getBlacklistDatabase() {
if (blacklistDatabase == null) {
blacklistDatabase = new SqlBrite.Builder().build()
.wrapDatabaseHelper(new BlacklistDbOpenHelper(ShuttleApplication.getInstance()), Schedulers.io());
}
return blacklistDatabase;
}
/**
* @return a <b>continuous</b> stream of {@link List<BlacklistedSong>>}, backed by a behavior relay for caching query results.
*/
private Observable<List<BlacklistedSong>> getBlacklistRelay() {
if (blacklistSubscription == null || blacklistSubscription.isUnsubscribed()) {
blacklistSubscription = getBlacklistDatabase()
.createQuery(BlacklistDbOpenHelper.TABLE_SONGS, "SELECT * FROM " + BlacklistDbOpenHelper.TABLE_SONGS)
.mapToList(BlacklistedSong::new)
.subscribe(blacklistRelay, error -> Crashlytics.log("getBlacklistRelay error: " + error.getMessage()));
}
return blacklistRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
/**
* @return a {@link BriteDatabase} wrapping the whitelist SqliteOpenHelper.
*/
public BriteDatabase getWhitelistDatabase() {
if (whitelistDatabase == null) {
whitelistDatabase = new SqlBrite.Builder().build()
.wrapDatabaseHelper(new WhitelistDbOpenHelper(ShuttleApplication.getInstance()), Schedulers.io());
}
return whitelistDatabase;
}
/**
* @return a <b>continuous</b> stream of {@link List<WhitelistFolder>>}, backed by a behavior relay for caching query results.
*/
private Observable<List<WhitelistFolder>> getWhitelistRelay() {
if (whitelistSubscription == null || whitelistSubscription.isUnsubscribed()) {
whitelistSubscription = getWhitelistDatabase()
.createQuery(WhitelistDbOpenHelper.TABLE_FOLDERS, "SELECT * FROM " + WhitelistDbOpenHelper.TABLE_FOLDERS)
.mapToList(WhitelistFolder::new)
.subscribe(whitelistRelay, error -> Crashlytics.log("getWhitelistRelay error: " + error.getMessage()));
}
return whitelistRelay.subscribeOn(Schedulers.io()).map(ArrayList::new);
}
}