package com.marverenic.music.data.store;
import android.content.Context;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.marverenic.music.R;
import com.marverenic.music.model.AutoPlaylist;
import com.marverenic.music.model.Playlist;
import com.marverenic.music.model.Song;
import com.marverenic.music.model.playlistrules.AutoPlaylistRule;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.exceptions.Exceptions;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import timber.log.Timber;
public class LocalPlaylistStore implements PlaylistStore {
private static final String AUTO_PLAYLIST_EXTENSION = ".jpl";
// Used to generate Auto Playlist contents
private MusicStore mMusicStore;
private PlayCountStore mPlayCountStore;
private Context mContext;
private BehaviorSubject<List<Playlist>> mPlaylists;
private Map<Playlist, BehaviorSubject<List<Song>>> mPlaylistContents;
private BehaviorSubject<Boolean> mLoadingState;
public LocalPlaylistStore(Context context, MusicStore musicStore,
PlayCountStore playCountStore) {
mContext = context;
mMusicStore = musicStore;
mPlayCountStore = playCountStore;
mPlaylistContents = new ArrayMap<>();
mLoadingState = 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.Playlists.EXTERNAL_CONTENT_URI)
.subscribe(selfChange -> refresh(), throwable -> {
Timber.e(throwable, "Failed to automatically refresh playlists");
});
}
@Override
public void loadPlaylists() {
getPlaylists().take(1).subscribe();
}
@Override
public Observable<Boolean> refresh() {
if (mPlaylists == null) {
return Observable.just(true);
}
mLoadingState.onNext(true);
BehaviorSubject<Boolean> result = BehaviorSubject.create();
MediaStoreUtil.promptPermission(mContext)
.observeOn(Schedulers.io())
.map(granted -> {
if (granted && mPlaylists != null) {
mPlaylists.onNext(getAllPlaylists());
mPlaylistContents.clear();
}
mLoadingState.onNext(false);
return granted;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result);
return result;
}
@Override
public Observable<Boolean> isLoading() {
return mLoadingState.asObservable().observeOn(AndroidSchedulers.mainThread());
}
@Override
public Observable<List<Playlist>> getPlaylists() {
if (mPlaylists == null) {
mPlaylists = BehaviorSubject.create();
mLoadingState.onNext(true);
MediaStoreUtil.getPermission(mContext)
.observeOn(Schedulers.io())
.subscribe(granted -> {
if (granted) {
mPlaylists.onNext(getAllPlaylists());
} else {
mPlaylists.onNext(Collections.emptyList());
}
mLoadingState.onNext(false);
}, throwable -> {
Timber.e(throwable, "Failed to query MediaStore for playlists");
});
}
return mPlaylists.asObservable().observeOn(AndroidSchedulers.mainThread());
}
private List<Playlist> getAllPlaylists() {
return MediaStoreUtil.getAllPlaylists(mContext);
}
@Override
public Observable<List<Song>> getSongs(Playlist playlist) {
if (playlist instanceof AutoPlaylist) {
return getAutoPlaylistSongs((AutoPlaylist) playlist);
} else {
return getPlaylistSongs(playlist);
}
}
private Observable<List<Song>> getPlaylistSongs(Playlist playlist) {
BehaviorSubject<List<Song>> subject;
if (mPlaylistContents.containsKey(playlist)) {
subject = mPlaylistContents.get(playlist);
} else {
subject = BehaviorSubject.create();
mPlaylistContents.put(playlist, subject);
Observable.fromCallable(() -> MediaStoreUtil.getPlaylistSongs(mContext, playlist))
.subscribe(subject::onNext, subject::onError);
}
return subject.asObservable();
}
private Observable<List<Song>> getAutoPlaylistSongs(AutoPlaylist playlist) {
BehaviorSubject<List<Song>> subject;
if (mPlaylistContents.containsKey(playlist)) {
subject = mPlaylistContents.get(playlist);
} else {
subject = BehaviorSubject.create();
mPlaylistContents.put(playlist, subject);
playlist.generatePlaylist(mMusicStore, this, mPlayCountStore)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subject::onNext, subject::onError);
subject.observeOn(Schedulers.io())
.subscribe(contents -> {
MediaStoreUtil.editPlaylist(mContext, playlist, contents);
}, throwable -> {
Timber.e(throwable, "Failed to save playlist contents");
});
}
return subject.asObservable();
}
@Override
public Observable<List<Playlist>> searchForPlaylists(String query) {
if (query == null || query.isEmpty()) {
return Observable.just(Collections.emptyList());
}
return getPlaylists().map(playlists -> {
List<Playlist> filtered = new ArrayList<>();
String lowerCaseQuery = query.toLowerCase();
for (Playlist playlist : playlists) {
if (playlist.getPlaylistName().toLowerCase().contains(lowerCaseQuery)) {
filtered.add(playlist);
}
}
return filtered;
});
}
@Override
public String verifyPlaylistName(String playlistName) {
if (playlistName == null || playlistName.trim().isEmpty()) {
return mContext.getString(R.string.error_hint_empty_playlist);
}
if (MediaStoreUtil.findPlaylistByName(mContext, playlistName) != null) {
return mContext.getString(R.string.error_hint_duplicate_playlist);
}
return null;
}
@Override
public Playlist makePlaylist(String name) {
return makePlaylist(name, null);
}
@Override
public AutoPlaylist makePlaylist(AutoPlaylist playlist) {
Playlist localReference = MediaStoreUtil.createPlaylist(mContext,
playlist.getPlaylistName(), Collections.emptyList());
AutoPlaylist created = new AutoPlaylist.Builder(playlist)
.setId(localReference.getPlaylistId())
.build();
saveAutoPlaylistConfiguration(created);
if (mPlaylists != null && mPlaylists.getValue() != null) {
List<Playlist> updatedPlaylists = new ArrayList<>(mPlaylists.getValue());
updatedPlaylists.add(created);
Collections.sort(updatedPlaylists);
mPlaylists.onNext(updatedPlaylists);
}
return created;
}
@Override
public Playlist makePlaylist(String name, @Nullable List<Song> songs) {
Playlist created = MediaStoreUtil.createPlaylist(mContext, name, songs);
if (mPlaylists != null && mPlaylists.getValue() != null) {
List<Playlist> updated = new ArrayList<>(mPlaylists.getValue());
updated.add(created);
Collections.sort(updated);
mPlaylists.onNext(updated);
}
return created;
}
@Override
public void removePlaylist(Playlist playlist) {
MediaStoreUtil.deletePlaylist(mContext, playlist);
mPlaylistContents.remove(playlist);
if (mPlaylists != null && mPlaylists.getValue() != null) {
List<Playlist> updated = new ArrayList<>(mPlaylists.getValue());
updated.remove(playlist);
mPlaylists.onNext(updated);
}
}
@Override
public void editPlaylist(Playlist playlist, List<Song> newSongs) {
MediaStoreUtil.editPlaylist(mContext, playlist, newSongs);
if (mPlaylistContents.containsKey(playlist)) {
mPlaylistContents.get(playlist).onNext(
Collections.unmodifiableList(new ArrayList<>(newSongs)));
}
}
@Override
public void editPlaylist(AutoPlaylist replacement) {
saveAutoPlaylistConfiguration(replacement);
if (mPlaylists != null && mPlaylists.getValue() != null) {
List<Playlist> updatedPlaylists = new ArrayList<>(mPlaylists.getValue());
int index = updatedPlaylists.indexOf(replacement);
updatedPlaylists.set(index, replacement);
mPlaylists.onNext(updatedPlaylists);
}
}
private void saveAutoPlaylistConfiguration(AutoPlaylist playlist) {
Observable.just(playlist)
.observeOn(Schedulers.io())
.subscribe(p -> {
try {
writeAutoPlaylistConfiguration(p);
} catch (IOException e) {
throw Exceptions.propagate(e);
}
}, throwable -> {
Timber.e(throwable, "Failed to write AutoPlaylist configuration");
});
// Write an initial set of values to the MediaStore so other apps can see this playlist
playlist.generatePlaylist(mMusicStore, this, mPlayCountStore)
.take(1)
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(contents -> {
editPlaylist(playlist, contents);
}, throwable -> {
Timber.e(throwable, "Failed to write AutoPlaylist contents");
});
}
private void writeAutoPlaylistConfiguration(AutoPlaylist playlist) throws IOException {
Gson gson = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(AutoPlaylistRule.class, new AutoPlaylistRule.RuleTypeAdapter())
.create();
FileWriter writer = null;
try {
String filename = playlist.getPlaylistName() + AUTO_PLAYLIST_EXTENSION;
String fullPath = mContext.getExternalFilesDir(null) + File.separator + filename;
writer = new FileWriter(fullPath);
writer.write(gson.toJson(playlist, AutoPlaylist.class));
} finally {
if (writer != null) {
writer.close();
}
}
}
@Override
public void addToPlaylist(Playlist playlist, Song song) {
MediaStoreUtil.appendToPlaylist(mContext, playlist, song);
if (mPlaylistContents.containsKey(playlist)) {
BehaviorSubject<List<Song>> observableContents = mPlaylistContents.get(playlist);
List<Song> updatedContents = new ArrayList<>(observableContents.getValue());
updatedContents.add(song);
observableContents.onNext(updatedContents);
}
}
@Override
public void addToPlaylist(Playlist playlist, List<Song> songs) {
MediaStoreUtil.appendToPlaylist(mContext, playlist, songs);
if (mPlaylistContents.containsKey(playlist)) {
BehaviorSubject<List<Song>> observableContents = mPlaylistContents.get(playlist);
List<Song> updatedContents = new ArrayList<>(observableContents.getValue());
updatedContents.addAll(songs);
observableContents.onNext(updatedContents);
}
}
}