package com.marverenic.music.data.store; import android.content.Context; import android.provider.MediaStore; import com.marverenic.music.model.Album; import com.marverenic.music.model.Artist; import com.marverenic.music.model.Genre; import com.marverenic.music.model.Song; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import rx.Observable; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; import rx.subjects.BehaviorSubject; import timber.log.Timber; public class LocalMusicStore implements MusicStore { private Context mContext; private PreferenceStore mPreferenceStore; private BehaviorSubject<Boolean> mSongLoadingState; private BehaviorSubject<Boolean> mArtistLoadingState; private BehaviorSubject<Boolean> mAlbumLoadingState; private BehaviorSubject<Boolean> mGenreLoadingState; private BehaviorSubject<List<Song>> mSongs; private BehaviorSubject<List<Album>> mAlbums; private BehaviorSubject<List<Artist>> mArtists; private BehaviorSubject<List<Genre>> mGenres; public LocalMusicStore(Context context, PreferenceStore preferenceStore) { mContext = context; mPreferenceStore = preferenceStore; mSongLoadingState = BehaviorSubject.create(false); mAlbumLoadingState = BehaviorSubject.create(false); mArtistLoadingState = BehaviorSubject.create(false); mGenreLoadingState = BehaviorSubject.create(false); MediaStoreUtil.waitForPermission() .subscribe(permission -> bindRefreshListener(), throwable -> { Timber.e(throwable, "Failed to bind refresh listener"); }); } private void bindRefreshListener() { MediaStoreUtil.getContentObserver(mContext, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) .subscribe(selfChange -> refreshSongs(), throwable -> { Timber.e(throwable, "Failed to automatically refresh songs"); }); MediaStoreUtil.getContentObserver(mContext, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI) .subscribe(selfChange -> refreshArtists(), throwable -> { Timber.e(throwable, "Failed to automatically refresh artists"); }); MediaStoreUtil.getContentObserver(mContext, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI) .subscribe(selfChange -> refreshAlbums(), throwable -> { Timber.e(throwable, "Failed to automatically refresh albums"); }); MediaStoreUtil.getContentObserver(mContext, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI) .subscribe(selfChange -> refreshGenres(), throwable -> { Timber.e(throwable, "Failed to automatically refresh genres"); }); } private void refreshSongs() { mSongLoadingState.onNext(true); MediaStoreUtil.promptPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted && mSongs != null) { mSongs.onNext(getAllSongs()); } mSongLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to refresh songs"); }); } private void refreshArtists() { mArtistLoadingState.onNext(true); MediaStoreUtil.promptPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted && mArtists != null) { mArtists.onNext(getAllArtists()); } mArtistLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to refresh artists"); }); } private void refreshAlbums() { mAlbumLoadingState.onNext(true); MediaStoreUtil.promptPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted && mAlbums != null) { mAlbums.onNext(getAllAlbums()); } mAlbumLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to refresh albums"); }); } private void refreshGenres() { mGenreLoadingState.onNext(true); MediaStoreUtil.promptPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted && mGenres != null) { mGenres.onNext(getAllGenres()); } mGenreLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to refresh genres"); }); } @Override public void loadAll() { getSongs().take(1).subscribe(); getArtists().take(1).subscribe(); getAlbums().take(1).subscribe(); getGenres().take(1).subscribe(); } @Override public Observable<Boolean> refresh() { mSongLoadingState.onNext(true); mArtistLoadingState.onNext(true); mAlbumLoadingState.onNext(true); mGenreLoadingState.onNext(true); BehaviorSubject<Boolean> result = BehaviorSubject.create(); MediaStoreUtil.promptPermission(mContext) .observeOn(Schedulers.io()) .map(granted -> { if (granted) { if (mSongs != null) { mSongs.onNext(getAllSongs()); } if (mArtists != null) { mArtists.onNext(getAllArtists()); } if (mAlbums != null) { mAlbums.onNext(getAllAlbums()); } if (mGenres != null) { mGenres.onNext(getAllGenres()); } } mSongLoadingState.onNext(false); mArtistLoadingState.onNext(false); mAlbumLoadingState.onNext(false); mGenreLoadingState.onNext(false); return granted; }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result); return result.asObservable(); } @Override public Observable<Boolean> isLoading() { return Observable.combineLatest(mSongLoadingState, mArtistLoadingState, mAlbumLoadingState, mGenreLoadingState, (songState, artistState, albumState, genreState) -> { return songState || artistState || albumState || genreState; }) .observeOn(AndroidSchedulers.mainThread()); } @Override public Observable<List<Song>> getSongs() { if (mSongs == null) { mSongs = BehaviorSubject.create(); mSongLoadingState.onNext(true); MediaStoreUtil.getPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted) { mSongs.onNext(getAllSongs()); } else { mSongs.onNext(Collections.emptyList()); } mSongLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to query MediaStore for songs"); }); } return mSongs.asObservable().observeOn(AndroidSchedulers.mainThread()); } private List<Song> getAllSongs() { return MediaStoreUtil.getSongs(mContext, getDirectoryInclusionExclusionSelection(), null); } private String getDirectoryInclusionExclusionSelection() { String selection; String includeSelection = getDirectoryInclusionSelection(); String excludeSelection = getDirectoryExclusionSelection(); if (includeSelection != null && excludeSelection != null) { selection = "(" + includeSelection + ") AND (" + excludeSelection + ")"; } else if (includeSelection != null) { selection = includeSelection; } else if (excludeSelection != null) { selection = excludeSelection; } else { selection = null; } return selection; } private String getDirectoryInclusionSelection() { if (mPreferenceStore.getIncludedDirectories().isEmpty()) { return null; } StringBuilder builder = new StringBuilder(); for (String directory : mPreferenceStore.getIncludedDirectories()) { builder.append(MediaStore.Audio.Media.DATA) .append(" LIKE \'") .append(directory).append(File.separatorChar) .append("%\'"); builder.append(" OR "); } builder.setLength(builder.length() - 4); return builder.toString(); } private String getDirectoryExclusionSelection() { if (mPreferenceStore.getExcludedDirectories().isEmpty()) { return null; } StringBuilder builder = new StringBuilder(); for (String directory : mPreferenceStore.getExcludedDirectories()) { builder.append(MediaStore.Audio.Media.DATA) .append(" NOT LIKE \'") .append(directory).append(File.separatorChar) .append("%\'"); builder.append(" AND "); } builder.setLength(builder.length() - 5); return builder.toString(); } @Override public Observable<List<Album>> getAlbums() { if (mAlbums == null) { mAlbums = BehaviorSubject.create(); mAlbumLoadingState.onNext(true); MediaStoreUtil.getPermission(mContext) .flatMap(granted -> { if (noDirectoryFilters()) { return Observable.just(granted); } else { return getSongs().map((List<Song> songs) -> granted); } }) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted) { mAlbums.onNext(getAllAlbums()); } else { mAlbums.onNext(Collections.emptyList()); } mAlbumLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to query MediaStore for albums"); }); } return mAlbums.asObservable().observeOn(AndroidSchedulers.mainThread()); } private List<Album> getAllAlbums() { return filterAlbums(MediaStoreUtil.getAlbums(mContext, null, null)); } @Override public Observable<List<Artist>> getArtists() { if (mArtists == null) { mArtists = BehaviorSubject.create(); mArtistLoadingState.onNext(true); MediaStoreUtil.getPermission(mContext) .flatMap(granted -> { if (noDirectoryFilters()) { return Observable.just(granted); } else { return getSongs().map((List<Song> songs) -> granted); } }) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted) { mArtists.onNext(getAllArtists()); } else { mArtists.onNext(Collections.emptyList()); } mArtistLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to query MediaStore for artists"); }); } return mArtists.asObservable().observeOn(AndroidSchedulers.mainThread()); } private List<Artist> getAllArtists() { return filterArtists(MediaStoreUtil.getArtists(mContext, null, null)); } @Override public Observable<List<Genre>> getGenres() { if (mGenres == null) { mGenres = BehaviorSubject.create(); mGenreLoadingState.onNext(true); MediaStoreUtil.getPermission(mContext) .observeOn(Schedulers.io()) .subscribe(granted -> { if (granted) { mGenres.onNext(getAllGenres()); } else { mGenres.onNext(Collections.emptyList()); } mGenreLoadingState.onNext(false); }, throwable -> { Timber.e(throwable, "Failed to query MediaStore for genres"); }); } return mGenres.asObservable().observeOn(AndroidSchedulers.mainThread()); } private List<Genre> getAllGenres() { return filterGenres(MediaStoreUtil.getGenres(mContext, null, null)); } private boolean noDirectoryFilters() { boolean notIncludingFolders = mPreferenceStore.getIncludedDirectories().isEmpty(); boolean notExcludingFolders = mPreferenceStore.getExcludedDirectories().isEmpty(); return notExcludingFolders && notIncludingFolders; } private List<Album> filterAlbums(List<Album> albumsToFilter) { if (noDirectoryFilters()) { return albumsToFilter; } List<Album> filteredAlbums = new ArrayList<>(); for (Album album : albumsToFilter) { for (Song song : mSongs.getValue()) { if (album.getAlbumId() == song.getAlbumId()) { filteredAlbums.add(album); break; } } } return filteredAlbums; } private List<Artist> filterArtists(List<Artist> artistsToFilter) { if (noDirectoryFilters()) { return artistsToFilter; } List<Artist> filteredArtists = new ArrayList<>(); for (Artist artist : artistsToFilter) { for (Song song : mSongs.getValue()) { if (artist.getArtistId() == song.getArtistId()) { filteredArtists.add(artist); break; } } } return filteredArtists; } private List<Genre> filterGenres(List<Genre> genresToFilter) { if (noDirectoryFilters()) { return genresToFilter; } List<Genre> filteredGenres = new ArrayList<>(); String directorySelection = getDirectoryInclusionExclusionSelection(); for (Genre genre : genresToFilter) { boolean hasSongs = !MediaStoreUtil.getGenreSongs(mContext, genre, directorySelection, null).isEmpty(); if (hasSongs) { filteredGenres.add(genre); } } return filteredGenres; } @Override public Observable<List<Song>> getSongs(Artist artist) { String selection = MediaStore.Audio.Media.ARTIST_ID + " = ?"; String[] selectionArgs = {Long.toString(artist.getArtistId())}; String directorySelection = getDirectoryInclusionExclusionSelection(); if (directorySelection != null) { selection += " AND " + directorySelection; } return Observable.just(MediaStoreUtil.getSongs(mContext, selection, selectionArgs)); } @Override public Observable<List<Song>> getSongs(Album album) { String selection = MediaStore.Audio.Media.ALBUM_ID + " = ? "; String[] selectionArgs = {Long.toString(album.getAlbumId())}; String directorySelection = getDirectoryInclusionExclusionSelection(); if (directorySelection != null) { selection += " AND " + directorySelection; } return Observable.just(MediaStoreUtil.getSongs(mContext, selection, selectionArgs)); } @Override public Observable<List<Song>> getSongs(Genre genre) { return Observable.just(MediaStoreUtil.getGenreSongs(mContext, genre, getDirectoryInclusionExclusionSelection(), null)); } @Override public Observable<List<Album>> getAlbums(Artist artist) { return Observable.just(filterAlbums(MediaStoreUtil.getArtistAlbums(mContext, artist))); } @Override public Observable<Artist> findArtistById(long artistId) { return Observable.just(MediaStoreUtil.findArtistById(mContext, artistId)); } @Override public Observable<Album> findAlbumById(long albumId) { return Observable.just(MediaStoreUtil.findAlbumById(mContext, albumId)); } @Override public Observable<Artist> findArtistByName(String artistName) { return Observable.just(MediaStoreUtil.findArtistByName(mContext, artistName)); } @Override public Observable<List<Song>> searchForSongs(String query) { if (query == null || query.isEmpty()) { return Observable.just(Collections.emptyList()); } return getSongs().map(songs -> { List<Song> filtered = new ArrayList<>(); String lowerCaseQuery = query.toLowerCase(); for (Song song : songs) { if (song.getSongName().toLowerCase().contains(lowerCaseQuery) || song.getAlbumName().toLowerCase().contains(lowerCaseQuery) || song.getArtistName().toLowerCase().contains(lowerCaseQuery)) { filtered.add(song); } } return filtered; }); } @Override public Observable<List<Artist>> searchForArtists(String query) { if (query == null || query.isEmpty()) { return Observable.just(Collections.emptyList()); } return getArtists().map(artists -> { List<Artist> filtered = new ArrayList<>(); String lowerCaseQuery = query.toLowerCase(); for (Artist artist : artists) { if (artist.getArtistName().toLowerCase().contains(lowerCaseQuery)) { filtered.add(artist); } } return filtered; }); } @Override public Observable<List<Album>> searchForAlbums(String query) { if (query == null || query.isEmpty()) { return Observable.just(Collections.emptyList()); } return getAlbums().map(albums -> { List<Album> filtered = new ArrayList<>(); String lowerCaseQuery = query.toLowerCase(); for (Album album : albums) { if (album.getAlbumName().toLowerCase().contains(lowerCaseQuery) || album.getArtistName().toLowerCase().contains(lowerCaseQuery)) { filtered.add(album); } } return filtered; }); } @Override public Observable<List<Genre>> searchForGenres(String query) { if (query == null || query.isEmpty()) { return Observable.just(Collections.emptyList()); } return getGenres().map(genres -> { List<Genre> filtered = new ArrayList<>(); String lowerCaseQuery = query.toLowerCase(); for (Genre genre : genres) { if (genre.getGenreName().toLowerCase().contains(lowerCaseQuery)) { filtered.add(genre); } } return filtered; }); } }