package com.simplecity.amp_library.search; import android.app.Activity; import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.bignerdranch.android.multiselector.MultiSelector; import com.bumptech.glide.RequestManager; import com.simplecity.amp_library.R; import com.simplecity.amp_library.ShuttleApplication; import com.simplecity.amp_library.format.PrefixHighlighter; import com.simplecity.amp_library.model.AdaptableItem; import com.simplecity.amp_library.model.Album; import com.simplecity.amp_library.model.AlbumArtist; import com.simplecity.amp_library.model.Header; import com.simplecity.amp_library.model.Playlist; import com.simplecity.amp_library.model.Song; import com.simplecity.amp_library.tagger.TaggerDialog; import com.simplecity.amp_library.ui.activities.MainActivity; import com.simplecity.amp_library.ui.adapters.SearchAdapter; import com.simplecity.amp_library.ui.modelviews.AlbumArtistView; import com.simplecity.amp_library.ui.modelviews.AlbumView; import com.simplecity.amp_library.ui.modelviews.SearchHeaderView; import com.simplecity.amp_library.ui.modelviews.SongView; import com.simplecity.amp_library.ui.modelviews.ViewType; import com.simplecity.amp_library.ui.presenters.Presenter; import com.simplecity.amp_library.utils.DataManager; import com.simplecity.amp_library.utils.DialogUtils; import com.simplecity.amp_library.utils.MusicUtils; import com.simplecity.amp_library.utils.Operators; import com.simplecity.amp_library.utils.PlaylistUtils; import com.simplecity.amp_library.utils.SettingsManager; import com.simplecity.amp_library.utils.ShuttleUtils; import com.simplecity.amp_library.utils.StringUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; class SearchPresenter extends Presenter<SearchView> implements SearchAdapter.SearchListener, Toolbar.OnMenuItemClickListener { private static final double SCORE_THRESHOLD = 0.80; private PrefixHighlighter prefixHighlighter; private RequestManager requestManager; private Subscription performSearchSubscription; private Subscription setItemsSubscription; private String query; SearchPresenter(PrefixHighlighter prefixHighlighter, RequestManager requestManager) { this.prefixHighlighter = prefixHighlighter; this.requestManager = requestManager; } @Override public void bindView(@NonNull SearchView view) { super.bindView(view); view.setFilterFuzzyChecked(SettingsManager.getInstance().getSearchFuzzy()); view.setFilterArtistsChecked(SettingsManager.getInstance().getSearchArtists()); view.setFilterAlbumsChecked(SettingsManager.getInstance().getSearchAlbums()); } @Override public void unbindView(@NonNull SearchView view) { super.unbindView(view); } void queryChanged(@Nullable String query) { if (TextUtils.isEmpty(query)) { query = ""; } loadData(query); this.query = query; } private void loadData(@NonNull String query) { SearchView searchView = getView(); if (searchView != null) { searchView.setLoading(true); //We've received a new refresh call. Unsubscribe the in-flight subscription if it exists. if (performSearchSubscription != null) { performSearchSubscription.unsubscribe(); } boolean searchArtists = SettingsManager.getInstance().getSearchArtists(); Observable<List<AdaptableItem>> albumArtistsObservable = searchArtists ? DataManager.getInstance().getAlbumArtistsRelay() .first() .lift(new AlbumArtistFilterOperator(query, requestManager, prefixHighlighter)) : Observable.just(Collections.emptyList()); boolean searchAlbums = SettingsManager.getInstance().getSearchAlbums(); Observable<List<AdaptableItem>> albumsObservable = searchAlbums ? DataManager.getInstance().getAlbumsRelay() .first() .lift(new AlbumFilterOperator(query, requestManager, prefixHighlighter)) : Observable.just(Collections.emptyList()); Observable<List<AdaptableItem>> songsObservable = DataManager.getInstance().getSongsRelay() .first() .lift(new SongFilterOperator(query, requestManager, prefixHighlighter)); performSearchSubscription = Observable.combineLatest( albumArtistsObservable, albumsObservable, songsObservable, (adaptableItems, adaptableItems2, adaptableItems3) -> { List<AdaptableItem> list = new ArrayList<>(); list.addAll(adaptableItems); list.addAll(adaptableItems2); list.addAll(adaptableItems3); return list; }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(adaptableItems -> { //We've got a new set of items to adapt.. Cancel the in-flight subscription. if (setItemsSubscription != null) { setItemsSubscription.unsubscribe(); } if (adaptableItems.isEmpty()) { searchView.setEmpty(true); } else { setItemsSubscription = searchView.setItems(adaptableItems); } }); addSubcscription(performSearchSubscription); } } void setSearchFuzzy(boolean searchFuzzy) { SettingsManager.getInstance().setSearchFuzzy(searchFuzzy); loadData(query); } void setSearchArtists(boolean searchArtists) { SettingsManager.getInstance().setSearchArtists(searchArtists); loadData(query); } void setSearchAlbums(boolean searchAlbums) { SettingsManager.getInstance().setSearchAlbums(searchAlbums); loadData(query); } @Override public boolean onMenuItemClick(MenuItem item) { return false; } @Override public void onItemClick(AlbumArtist albumArtist) { SearchView view = getView(); Intent intent = new Intent(); intent.putExtra(MainActivity.ARG_MODEL, albumArtist); if (view != null) { view.finish(Activity.RESULT_OK, intent); } } @Override public void onItemClick(Album album) { SearchView view = getView(); Intent intent = new Intent(); intent.putExtra(MainActivity.ARG_MODEL, album); if (view != null) { view.finish(Activity.RESULT_OK, intent); } } @Override public void onItemClick(Song song, List<Song> allSongs) { SearchView view = getView(); int index = allSongs.indexOf(song); MusicUtils.playAll(allSongs, index, false, () -> { if (view != null) { view.showEmptyPlaylistToast(); } }); if (view != null) { view.finish(); } } @Override public void onOverflowClick(View v, AlbumArtist albumArtist) { PopupMenu menu = new PopupMenu(v.getContext(), v); menu.getMenu().add(0, MusicUtils.Defs.PLAY_SELECTION, 0, R.string.play_selection); SubMenu sub = menu.getMenu().addSubMenu(0, MusicUtils.Defs.ADD_TO_PLAYLIST, 1, R.string.add_to_playlist); PlaylistUtils.makePlaylistMenu(v.getContext(), sub, 0); menu.getMenu().add(0, MusicUtils.Defs.QUEUE, 2, R.string.add_to_queue); if (ShuttleUtils.isUpgraded()) { menu.getMenu().add(0, MusicUtils.Defs.TAGGER, 3, R.string.edit_tags); } menu.getMenu().add(0, MusicUtils.Defs.DELETE_ITEM, 6, R.string.delete_item); menu.setOnMenuItemClickListener(item -> { SearchView view = getView(); Observable<List<Song>> songsObservable = albumArtist.getSongsObservable(); switch (item.getItemId()) { case MusicUtils.Defs.PLAY_SELECTION: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playAll(songs, () -> { if (view != null) { view.showEmptyPlaylistToast(); } })); return true; case MusicUtils.Defs.PLAY_NEXT: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playNext(v.getContext(), songs)); return true; case MusicUtils.Defs.NEW_PLAYLIST: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> PlaylistUtils.createPlaylistDialog(v.getContext(), songs)); return true; case MusicUtils.Defs.PLAYLIST_SELECTED: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> { Playlist playlist = (Playlist) item.getIntent().getSerializableExtra(ShuttleUtils.ARG_PLAYLIST); PlaylistUtils.addToPlaylist(v.getContext(), playlist, songs); }); return true; case MusicUtils.Defs.QUEUE: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.addToQueue(v.getContext(), songs)); return true; case MusicUtils.Defs.TAGGER: if (view != null) { view.showTaggerDialog(TaggerDialog.newInstance(albumArtist)); } return true; case MusicUtils.Defs.DELETE_ITEM: if (view != null) { view.showDeleteDialog(new DialogUtils.DeleteDialogBuilder() .context(v.getContext()) .songsToDelete(songsObservable) .singleMessageId(R.string.delete_album_artist_desc) .multipleMessage(R.string.delete_album_artist_desc_multiple) .itemNames(Collections.singletonList((albumArtist.name))) .build()); } return true; } return false; } ); menu.show(); } @Override public void onOverflowClick(View v, Album album) { PopupMenu menu = new PopupMenu(v.getContext(), v); menu.getMenu().add(0, MusicUtils.Defs.PLAY_SELECTION, 0, R.string.play_selection); SubMenu sub = menu.getMenu().addSubMenu(0, MusicUtils.Defs.ADD_TO_PLAYLIST, 1, R.string.add_to_playlist); PlaylistUtils.makePlaylistMenu(v.getContext(), sub, 0); menu.getMenu().add(0, MusicUtils.Defs.QUEUE, 2, R.string.add_to_queue); if (ShuttleUtils.isUpgraded()) { menu.getMenu().add(0, MusicUtils.Defs.TAGGER, 3, R.string.edit_tags); } menu.getMenu().add(0, MusicUtils.Defs.DELETE_ITEM, 6, R.string.delete_item); menu.setOnMenuItemClickListener(item -> { SearchView view = getView(); Observable<List<Song>> songsObservable = (album.getSongsObservable()); switch (item.getItemId()) { case MusicUtils.Defs.PLAY_SELECTION: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playAll(songs, () -> { if (view != null) { view.showEmptyPlaylistToast(); } })); return true; case MusicUtils.Defs.PLAY_NEXT: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playNext(v.getContext(), songs)); return true; case MusicUtils.Defs.NEW_PLAYLIST: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> PlaylistUtils.createPlaylistDialog(v.getContext(), songs)); return true; case MusicUtils.Defs.PLAYLIST_SELECTED: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> { Playlist playlist = (Playlist) item.getIntent().getSerializableExtra(ShuttleUtils.ARG_PLAYLIST); PlaylistUtils.addToPlaylist(v.getContext(), playlist, songs); }); return true; case MusicUtils.Defs.QUEUE: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.addToQueue(v.getContext(), songs)); return true; case MusicUtils.Defs.TAGGER: if (view != null) { view.showTaggerDialog(TaggerDialog.newInstance(album)); } return true; case MusicUtils.Defs.DELETE_ITEM: if (view != null) { view.showDeleteDialog(new DialogUtils.DeleteDialogBuilder() .context(v.getContext()) .songsToDelete(songsObservable) .singleMessageId(R.string.delete_album_desc) .multipleMessage(R.string.delete_album_desc_multiple) .itemNames(Collections.singletonList((album.name))) .build()); } return true; } return false; } ); menu.show(); } @Override public void onOverflowClick(View v, Song song) { PopupMenu menu = new PopupMenu(v.getContext(), v); menu.getMenu().add(0, MusicUtils.Defs.PLAY_NEXT, 0, R.string.play_next); menu.getMenu().add(0, MusicUtils.Defs.USE_AS_RINGTONE, 4, R.string.ringtone_menu); SubMenu sub = menu.getMenu().addSubMenu(0, MusicUtils.Defs.ADD_TO_PLAYLIST, 1, R.string.add_to_playlist); PlaylistUtils.makePlaylistMenu(v.getContext(), sub, 0); menu.getMenu().add(0, MusicUtils.Defs.QUEUE, 2, R.string.add_to_queue); if (ShuttleUtils.isUpgraded()) { menu.getMenu().add(0, MusicUtils.Defs.TAGGER, 3, R.string.edit_tags); } menu.getMenu().add(0, MusicUtils.Defs.DELETE_ITEM, 6, R.string.delete_item); menu.setOnMenuItemClickListener(item -> { SearchView view = getView(); Observable<List<Song>> songsObservable = Observable.just(Collections.singletonList(song)); switch (item.getItemId()) { case MusicUtils.Defs.PLAY_SELECTION: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playAll(songs, () -> { if (view != null) { view.showEmptyPlaylistToast(); } })); return true; case MusicUtils.Defs.PLAY_NEXT: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.playNext(v.getContext(), songs)); return true; case MusicUtils.Defs.NEW_PLAYLIST: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> PlaylistUtils.createPlaylistDialog(v.getContext(), songs)); return true; case MusicUtils.Defs.PLAYLIST_SELECTED: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> { Playlist playlist = (Playlist) item.getIntent().getSerializableExtra(ShuttleUtils.ARG_PLAYLIST); PlaylistUtils.addToPlaylist(v.getContext(), playlist, songs); }); return true; case MusicUtils.Defs.USE_AS_RINGTONE: // Set the system setting to make this the current // ringtone ShuttleUtils.setRingtone(v.getContext(), song); return true; case MusicUtils.Defs.QUEUE: songsObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(songs -> MusicUtils.addToQueue(v.getContext(), songs)); return true; case MusicUtils.Defs.TAGGER: if (view != null) { view.showTaggerDialog(TaggerDialog.newInstance(song)); } return true; case MusicUtils.Defs.DELETE_ITEM: if (view != null) { view.showDeleteDialog(new DialogUtils.DeleteDialogBuilder() .context(v.getContext()) .songsToDelete(songsObservable) .singleMessageId(R.string.delete_song_desc) .multipleMessage(R.string.delete_song_desc_multiple) .itemNames(Collections.singletonList((song.name))) .build()); } return true; } return false; } ); menu.show(); } private static class SongFilterOperator implements Observable.Operator<List<AdaptableItem>, List<Song>> { private String filterString; private RequestManager requestManager; private PrefixHighlighter prefixHighlighter; private MultiSelector dummySelector = new MultiSelector(); private SearchHeaderView songsHeader = new SearchHeaderView(new Header(ShuttleApplication.getInstance().getString(R.string.tracks_title))); SongFilterOperator(@NonNull String filterString, @NonNull RequestManager requestManager, @NonNull PrefixHighlighter prefixHighlighter) { this.filterString = filterString; this.requestManager = requestManager; this.prefixHighlighter = prefixHighlighter; } @Override public Subscriber<List<Song>> call(Subscriber<? super List<AdaptableItem>> subscriber) { return new Subscriber<List<Song>>() { @Override public void onNext(List<Song> songs) { char[] prefix = filterString.toUpperCase().toCharArray(); List<Album> albums = Operators.songsToAlbums(songs); Collections.sort(albums, Album::compareTo); List<AlbumArtist> albumArtists = Operators.albumsToAlbumArtists(albums); Collections.sort(albumArtists, AlbumArtist::compareTo); if (isUnsubscribed()) return; boolean fuzzy = SettingsManager.getInstance().getSearchFuzzy(); Stream<Song> songStream = Stream.of(songs) .filter(song -> song.name != null); Stream<Song> filteredStream = fuzzy ? applyJaroWinklerFilter(songStream) : applySongFilter(songStream); List<AdaptableItem> adaptableItems = filteredStream.map(song -> { SongView songView = new SongView(song, dummySelector, requestManager); songView.setPrefix(prefixHighlighter, prefix); return songView; }) .collect(Collectors.toList()); if (!adaptableItems.isEmpty()) { adaptableItems.add(0, songsHeader); } if (!subscriber.isUnsubscribed()) { subscriber.onNext(adaptableItems); } } @Override public void onCompleted() { subscriber.onCompleted(); } @Override public void onError(Throwable e) { subscriber.onError(e); } }; } private Stream<Song> applyJaroWinklerFilter(Stream<Song> songStream) { return songStream.map(song -> new SearchUtils.JaroWinklerObject<>(song, filterString, song.name)) .filter(jaroWinklerObject -> jaroWinklerObject.score > SCORE_THRESHOLD || TextUtils.isEmpty(filterString)) .sorted((a, b) -> a.object.compareTo(b.object)) .sorted((a, b) -> Double.compare(b.score, a.score)) .map(jaroWinklerObject -> jaroWinklerObject.object); } private Stream<Song> applySongFilter(Stream<Song> songStream) { return songStream.filter(song -> StringUtils.containsIgnoreCase(song.name, filterString)); } } private static class AlbumFilterOperator implements Observable.Operator<List<AdaptableItem>, List<Album>> { private String filterString; private RequestManager requestManager; private PrefixHighlighter prefixHighlighter; private SearchHeaderView albumsHeader = new SearchHeaderView(new Header(ShuttleApplication.getInstance().getString(R.string.albums_title))); AlbumFilterOperator(@NonNull String filterString, @NonNull RequestManager requestManager, @NonNull PrefixHighlighter prefixHighlighter) { this.filterString = filterString; this.requestManager = requestManager; this.prefixHighlighter = prefixHighlighter; } @Override public Subscriber<? super List<Album>> call(Subscriber<? super List<AdaptableItem>> subscriber) { return new Subscriber<List<Album>>() { @Override public void onNext(List<Album> albums) { char[] prefix = filterString.toUpperCase().toCharArray(); Collections.sort(albums, Album::compareTo); if (isUnsubscribed()) return; boolean fuzzy = SettingsManager.getInstance().getSearchFuzzy(); Stream<Album> albumStream = Stream.of(albums) .filter(album -> album.name != null); Stream<Album> filteredStream = fuzzy ? applyJaroWinklerAlbumFilter(albumStream) : applyAlbumFilter(albumStream); List<AdaptableItem> adaptableItems = filteredStream.map(album -> { AlbumView albumView = new AlbumView(album, ViewType.ALBUM_LIST, requestManager); albumView.setPrefix(prefixHighlighter, prefix); return albumView; }).collect(Collectors.toList()); if (!adaptableItems.isEmpty()) { adaptableItems.add(0, albumsHeader); } if (!subscriber.isUnsubscribed()) { subscriber.onNext(adaptableItems); } } @Override public void onCompleted() { subscriber.onCompleted(); } @Override public void onError(Throwable e) { subscriber.onError(e); } }; } private Stream<Album> applyJaroWinklerAlbumFilter(Stream<Album> stream) { return stream.map(album -> new SearchUtils.JaroWinklerObject<>(album, filterString, album.name)) .filter(jaroWinklerObject -> jaroWinklerObject.score > SCORE_THRESHOLD || TextUtils.isEmpty(filterString)) .sorted((a, b) -> a.object.compareTo(b.object)) .sorted((a, b) -> Double.compare(b.score, a.score)) .map(jaroWinklerObject -> jaroWinklerObject.object); } private Stream<Album> applyAlbumFilter(Stream<Album> stream) { return stream.filter(album -> StringUtils.containsIgnoreCase(album.name, filterString)); } } private static class AlbumArtistFilterOperator implements Observable.Operator<List<AdaptableItem>, List<AlbumArtist>> { private String filterString; private RequestManager requestManager; private PrefixHighlighter prefixHighlighter; private SearchHeaderView artistsHeader = new SearchHeaderView(new Header(ShuttleApplication.getInstance().getString(R.string.artists_title))); AlbumArtistFilterOperator(@NonNull String filterString, @NonNull RequestManager requestManager, @NonNull PrefixHighlighter prefixHighlighter) { this.filterString = filterString; this.requestManager = requestManager; this.prefixHighlighter = prefixHighlighter; } @Override public Subscriber<? super List<AlbumArtist>> call(Subscriber<? super List<AdaptableItem>> subscriber) { return new Subscriber<List<AlbumArtist>>() { @Override public void onNext(List<AlbumArtist> albumArtists) { char[] prefix = filterString.toUpperCase().toCharArray(); Collections.sort(albumArtists, AlbumArtist::compareTo); if (isUnsubscribed()) return; boolean fuzzy = SettingsManager.getInstance().getSearchFuzzy(); Stream<AlbumArtist> albumArtistStream = Stream.of(albumArtists) .filter(albumArtist -> albumArtist.name != null); Stream<AlbumArtist> filteredStream = fuzzy ? applyJaroWinklerAlbumArtistFilter(albumArtistStream) : applyAlbumArtistFilter(albumArtistStream); List<AdaptableItem> adaptableItems = filteredStream .map(albumArtist -> { AlbumArtistView albumArtistView = new AlbumArtistView(albumArtist, ViewType.ARTIST_LIST, requestManager); albumArtistView.setPrefix(prefixHighlighter, prefix); return (AdaptableItem) albumArtistView; }) .collect(Collectors.toList()); if (!adaptableItems.isEmpty()) { adaptableItems.add(0, artistsHeader); } if (!subscriber.isUnsubscribed()) { subscriber.onNext(adaptableItems); } } @Override public void onCompleted() { subscriber.onCompleted(); } @Override public void onError(Throwable e) { subscriber.onError(e); } }; } private Stream<AlbumArtist> applyJaroWinklerAlbumArtistFilter(Stream<AlbumArtist> stream) { return stream.map(albumArtist -> new SearchUtils.JaroWinklerObject<>(albumArtist, filterString, albumArtist.name)) .filter(jaroWinklerObject -> jaroWinklerObject.score > SCORE_THRESHOLD || TextUtils.isEmpty(filterString)) .sorted((a, b) -> a.object.compareTo(b.object)) .sorted((a, b) -> Double.compare(b.score, a.score)) .map(jaroWinklerObject -> jaroWinklerObject.object); } private Stream<AlbumArtist> applyAlbumArtistFilter(Stream<AlbumArtist> stream) { return stream.filter(albumArtist -> StringUtils.containsIgnoreCase(albumArtist.name, filterString)); } } }