package com.marverenic.music.activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuItem;
import com.marverenic.adapter.HeterogeneousAdapter;
import com.marverenic.music.JockeyApplication;
import com.marverenic.music.R;
import com.marverenic.music.adapter.AlbumSection;
import com.marverenic.music.adapter.ArtistSection;
import com.marverenic.music.adapter.BasicEmptyState;
import com.marverenic.music.adapter.GenreSection;
import com.marverenic.music.adapter.HeaderSection;
import com.marverenic.music.adapter.PlaylistSection;
import com.marverenic.music.adapter.SongSection;
import com.marverenic.music.data.store.MusicStore;
import com.marverenic.music.data.store.PlaylistStore;
import com.marverenic.music.model.Album;
import com.marverenic.music.model.Artist;
import com.marverenic.music.model.Genre;
import com.marverenic.music.model.Playlist;
import com.marverenic.music.model.Song;
import com.marverenic.music.player.PlayerController;
import com.marverenic.music.view.BackgroundDecoration;
import com.marverenic.music.view.DividerDecoration;
import com.marverenic.music.view.GridSpacingDecoration;
import com.marverenic.music.view.ViewUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import timber.log.Timber;
public class SearchActivity extends BaseLibraryActivity implements SearchView.OnQueryTextListener {
private static final String KEY_SAVED_QUERY = "SearchActivity.LAST_QUERY";
public static Intent newIntent(Context context) {
return new Intent(context, SearchActivity.class);
}
@Inject MusicStore mMusicStore;
@Inject PlaylistStore mPlaylistStore;
@Inject PlayerController mPlayerController;
private SearchView searchView;
private BehaviorSubject<String> mQueryObservable;
private RecyclerView mRecyclerView;
private HeterogeneousAdapter mAdapter;
private PlaylistSection mPlaylistSection;
private SongSection mSongSection;
private AlbumSection mAlbumSection;
private ArtistSection mArtistSection;
private GenreSection mGenreSection;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JockeyApplication.getComponent(this).inject(this);
String lastQuery;
if (savedInstanceState != null) {
lastQuery = savedInstanceState.getString(KEY_SAVED_QUERY);
} else {
lastQuery = "";
}
mQueryObservable = BehaviorSubject.create(lastQuery);
// Set up the RecyclerView's adapter
mRecyclerView = (RecyclerView) findViewById(R.id.list);
initAdapter();
mQueryObservable
.subscribeOn(Schedulers.io())
.flatMap(query -> mPlaylistStore.searchForPlaylists(query))
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(playlists -> {
mPlaylistSection.setData(playlists);
mAdapter.notifyDataSetChanged();
}, throwable -> {
Timber.e(throwable, "Failed to search for playlists");
});
mQueryObservable
.subscribeOn(Schedulers.io())
.flatMap(query -> mMusicStore.searchForSongs(query))
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(songs -> {
mSongSection.setData(songs);
mAdapter.notifyDataSetChanged();
}, throwable -> {
Timber.e(throwable, "Failed to search for songs");
});
mQueryObservable
.subscribeOn(Schedulers.io())
.flatMap(query -> mMusicStore.searchForAlbums(query))
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(albums -> {
mAlbumSection.setData(albums);
mAdapter.notifyDataSetChanged();
}, throwable -> {
Timber.e(throwable, "Failed to search for albums");
});
mQueryObservable
.subscribeOn(Schedulers.io())
.flatMap(query -> mMusicStore.searchForArtists(query))
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(artists -> {
mArtistSection.setData(artists);
mAdapter.notifyDataSetChanged();
}, throwable -> {
Timber.e(throwable, "Failed to search for artists");
});
mQueryObservable
.subscribeOn(Schedulers.io())
.flatMap(query -> mMusicStore.searchForGenres(query))
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(genres -> {
mGenreSection.setData(genres);
mAdapter.notifyDataSetChanged();
}, throwable -> {
Timber.e(throwable, "Failed to search for genres");
});
handleIntent(getIntent());
}
@Override
protected int getContentLayoutResource() {
return R.layout.activity_instance;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_SAVED_QUERY, mQueryObservable.getValue());
}
private void initAdapter() {
mPlaylistSection = new PlaylistSection(Collections.emptyList());
mSongSection = new SongSection(this, Collections.emptyList());
mAlbumSection = new AlbumSection(this, Collections.emptyList());
mArtistSection = new ArtistSection(this, Collections.emptyList());
mGenreSection = new GenreSection(this, Collections.emptyList());
mAdapter = new HeterogeneousAdapter()
.addSection(new HeaderSection(getString(R.string.header_playlists)))
.addSection(mPlaylistSection)
.addSection(new HeaderSection(getString(R.string.header_songs)))
.addSection(mSongSection)
.addSection(new HeaderSection(getString(R.string.header_albums)))
.addSection(mAlbumSection)
.addSection(new HeaderSection(getString(R.string.header_artists)))
.addSection(mArtistSection)
.addSection(new HeaderSection(getString(R.string.header_genres)))
.addSection(mGenreSection);
mAdapter.setEmptyState(new BasicEmptyState() {
@Override
public String getMessage() {
String query = mQueryObservable.getValue();
return (query == null || query.isEmpty())
? ""
: getString(R.string.empty_search);
}
});
mRecyclerView.setAdapter(mAdapter);
final int numColumns = ViewUtils.getNumberOfGridColumns(this, R.dimen.grid_width);
GridLayoutManager layoutManager = new GridLayoutManager(this, numColumns);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (mAdapter.getItemViewType(position) == mAlbumSection.getTypeId()) {
return 1;
}
return numColumns;
}
});
mRecyclerView.setLayoutManager(layoutManager);
// Add item decorations
mRecyclerView.addItemDecoration(new GridSpacingDecoration(
(int) getResources().getDimension(R.dimen.grid_margin),
numColumns, mAlbumSection.getTypeId()));
mRecyclerView.addItemDecoration(
new BackgroundDecoration(R.id.subheader_frame));
mRecyclerView.addItemDecoration(
new DividerDecoration(this,
R.id.album_view, R.id.subheader_frame, R.id.empty_layout));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_search, menu);
MenuItem searchItem = menu.findItem(R.id.menu_library_search);
searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
searchView.setIconified(false);
String query = mQueryObservable.getValue();
if (query != null && !query.isEmpty()) {
searchView.setQuery(query, true);
} else {
searchView.requestFocus();
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
navigateHome();
return true;
case R.id.menu_library_search:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) onSearchRequested();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void navigateHome() {
Intent mainActivity = new Intent(this, MainActivity.class);
mainActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(mainActivity);
}
@Override
public boolean onQueryTextSubmit(String query) {
search(query);
searchView.clearFocus();
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
search(newText);
return true;
}
private void search(String query) {
if (!mQueryObservable.getValue().equals(query)) {
mQueryObservable.onNext(query);
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private List<Playlist> getPlaylistResults() {
return mPlaylistSection.getData();
}
private List<Song> getSongResults() {
return mSongSection.getData();
}
private List<Artist> getArtistResults() {
return mArtistSection.getData();
}
private List<Album> getAlbumResults() {
return mAlbumSection.getData();
}
private List<Genre> getGenreResults() {
return mGenreSection.getData();
}
private void handleIntent(Intent intent) {
if (intent != null) {
// Handle standard searches
if (Intent.ACTION_SEARCH.equals(intent.getAction())
|| MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(intent.getAction())) {
search(intent.getStringExtra(SearchManager.QUERY));
} else if (MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH.equals(intent.getAction())) {
// Handle play from search actions
search(intent.getStringExtra(SearchManager.QUERY));
String focus = intent.getStringExtra(MediaStore.EXTRA_MEDIA_FOCUS);
String query = mQueryObservable.getValue();
if (MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE.equals(focus)) {
playPlaylistResults(query);
} else if (MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE.equals(focus)) {
playArtistResults();
} else if (MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE.equals(focus)) {
playAlbumResults(query);
} else if (focus.equals(MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE)) {
playGenreResults(query);
} else {
playSongResults();
}
}
}
}
private void playSongResults() {
if (!getSongResults().isEmpty()) {
mPlayerController.setQueue(getSongResults(), 0);
mPlayerController.play();
}
}
private void playPlaylistResults(String query) {
if (getPlaylistResults().isEmpty()) {
return;
}
// If there is a playlist with this exact name, use it, otherwise fallback
// to the first result
Playlist playlist = getPlaylistResults().get(0);
for (Playlist p : getPlaylistResults()) {
if (p.getPlaylistName().equalsIgnoreCase(query)) {
playlist = p;
break;
}
}
mPlaylistStore.getSongs(playlist).subscribe(
songs -> {
mPlayerController.setQueue(songs, 0);
mPlayerController.play();
}, throwable -> {
Timber.e(throwable, "Failed to play playlist from intent");
});
}
private void playArtistResults() {
if (getGenreResults().isEmpty()) {
return;
}
// If one or more artists with this name exist, play songs by all of them (Ideally this only
// includes collaborating artists and keeps the search relevant)
Observable<List<Song>> combinedSongs = Observable.just(new ArrayList<>());
for (Artist a : getArtistResults()) {
combinedSongs = Observable.combineLatest(
combinedSongs, mMusicStore.getSongs(a), (left, right) -> {
left.addAll(right);
return left;
});
}
combinedSongs.subscribe(
songs -> {
mPlayerController.setQueue(songs, 0);
mPlayerController.play();
},
throwable -> {
Timber.e(throwable, "Failed to play artist from intent");
});
}
private void playAlbumResults(String query) {
if (getAlbumResults().isEmpty()) {
return;
}
// If albums with this name exist, look for an exact match
// If we find one then use it, otherwise fallback to the first result
Album album = getAlbumResults().get(0);
for (Album a : getAlbumResults()) {
if (a.getAlbumName().equalsIgnoreCase(query)) {
album = a;
break;
}
}
mMusicStore.getSongs(album).subscribe(
songs -> {
mPlayerController.setQueue(songs, 0);
mPlayerController.play();
}, throwable -> {
Timber.e(throwable, "Failed to play album from intent");
});
}
private void playGenreResults(String query) {
if (!getGenreResults().isEmpty()) {
return;
}
// If genres with this name exist, look for an exact match
// If we find one then use it, otherwise fallback to the first result
Genre genre = getGenreResults().get(0);
for (Genre g : getGenreResults()) {
if (g.getGenreName().equalsIgnoreCase(query)) {
genre = g;
break;
}
}
mMusicStore.getSongs(genre).subscribe(
songs -> {
mPlayerController.setQueue(songs, 0);
mPlayerController.play();
}, throwable -> {
Timber.e(throwable, "Failed to play genre from intent");
});
}
}