/* * Copyright (C) 2015 Fastboot Mobile, LLC. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, see <http://www.gnu.org/licenses>. */ package com.fastbootmobile.encore.app.tv; import android.Manifest; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.SearchFragment; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ImageCardView; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.ObjectAdapter; import android.support.v17.leanback.widget.OnItemViewClickedListener; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.SpeechRecognitionCallback; import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.graphics.Palette; import android.text.TextUtils; import android.util.Log; import com.fastbootmobile.encore.app.BuildConfig; import com.fastbootmobile.encore.app.R; import com.fastbootmobile.encore.framework.PlaybackProxy; import com.fastbootmobile.encore.model.Album; import com.fastbootmobile.encore.model.Artist; import com.fastbootmobile.encore.model.Playlist; import com.fastbootmobile.encore.model.SearchResult; import com.fastbootmobile.encore.model.Song; import com.fastbootmobile.encore.providers.ILocalCallback; import com.fastbootmobile.encore.providers.IMusicProvider; import com.fastbootmobile.encore.providers.ProviderAggregator; import java.util.List; public class TvSearchFragment extends SearchFragment implements SearchFragment.SearchResultProvider { private static final String TAG = "TvSearchFragment"; private static final boolean DEBUG = BuildConfig.DEBUG; private static final int SEARCH_DELAY_MS = 1000; private static final boolean FINISH_ON_RECOGNIZER_CANCELED = true; private static final int REQUEST_SPEECH = 0x00000010; private ArrayObjectAdapter mRowsAdapter; private String mQuery; private Handler mHandler = new Handler(); private final Runnable mDelayedLoad = new Runnable() { @Override public void run() { loadRows(); } }; private final Runnable mUpdateAdapter = new Runnable() { @Override public void run() { final int numSubs = mRowsAdapter.size(); for (int i = 0; i < numSubs; ++i) { ListRow row = (ListRow) mRowsAdapter.get(i); ArrayObjectAdapter adapter = (ArrayObjectAdapter) row.getAdapter(); adapter.notifyArrayItemRangeChanged(0, adapter.size()); } } }; private ILocalCallback mCallback = new SearchLocalCallback(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); setSearchResultProvider(this); setOnItemViewClickedListener(new OnItemViewClickedListener() { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Album) { Album album = (Album) item; int color = getResources().getColor(R.color.primary); if (itemViewHolder.view.getTag() != null && itemViewHolder.view.getTag() instanceof Palette) { color = ((Palette) itemViewHolder.view.getTag()).getDarkVibrantColor(color); } Intent intent = new Intent(getActivity(), TvAlbumDetailsActivity.class); intent.putExtra(TvAlbumDetailsActivity.EXTRA_ALBUM, album); intent.putExtra(TvAlbumDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( getActivity(), ((ImageCardView) itemViewHolder.view).getMainImageView(), TvAlbumDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); startActivity(intent, bundle); } else if (item instanceof Artist) { Artist artist = (Artist) item; int color = getResources().getColor(R.color.primary); if (itemViewHolder.view.getTag() != null && itemViewHolder.view.getTag() instanceof Palette) { color = ((Palette) itemViewHolder.view.getTag()).getDarkVibrantColor(color); } Intent intent = new Intent(getActivity(), TvArtistDetailsActivity.class); intent.putExtra(TvArtistDetailsActivity.EXTRA_ARTIST, artist); intent.putExtra(TvArtistDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( getActivity(), ((ImageCardView) itemViewHolder.view).getMainImageView(), TvArtistDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); startActivity(intent, bundle); } else if (item instanceof Playlist) { Playlist playlist = (Playlist) item; int color = getResources().getColor(R.color.primary); if (itemViewHolder.view.getTag() != null && itemViewHolder.view.getTag() instanceof Palette) { color = ((Palette) itemViewHolder.view.getTag()).getDarkVibrantColor(color); } Intent intent = new Intent(getActivity(), TvPlaylistDetailsActivity.class); intent.putExtra(TvPlaylistDetailsActivity.EXTRA_PLAYLIST, playlist); intent.putExtra(TvPlaylistDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( getActivity(), ((ImageCardView) itemViewHolder.view).getMainImageView(), TvAlbumDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); startActivity(intent, bundle); } else if (item instanceof Song) { PlaybackProxy.playSong((Song) item); } } }); if (!hasPermission(Manifest.permission.RECORD_AUDIO)) { // SpeechRecognitionCallback is not required and if not provided recognition will be handled // using internal speech recognizer, in which case you must have RECORD_AUDIO permission setSpeechRecognitionCallback(new SpeechRecognitionCallback() { @Override public void recognizeSpeech() { if (DEBUG) Log.v(TAG, "recognizeSpeech"); try { startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH); } catch (ActivityNotFoundException e) { Log.e(TAG, "Cannot find activity for speech recognizer", e); } } }); } } @Override public void onPause() { mHandler.removeCallbacksAndMessages(null); ProviderAggregator.getDefault().removeUpdateCallback(mCallback); super.onPause(); } @Override public void onResume() { super.onResume(); ProviderAggregator.getDefault().addUpdateCallback(mCallback); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (DEBUG) Log.v(TAG, "onActivityResult requestCode=" + requestCode + " resultCode=" + resultCode + " data=" + data); switch (requestCode) { case REQUEST_SPEECH: switch (resultCode) { case Activity.RESULT_OK: setSearchQuery(data, true); break; case Activity.RESULT_CANCELED: // Once recognizer canceled, user expects the current activity to process // the same BACK press as user doesn't know about overlay activity. // However, you may not want this behaviour as it makes harder to // fall back to keyboard input. if (FINISH_ON_RECOGNIZER_CANCELED) { if (!hasResults()) { if (DEBUG) Log.v(TAG, "Delegating BACK press from recognizer"); getActivity().onBackPressed(); } } break; // the rest includes various recognizer errors, see {@link RecognizerIntent} } break; } } @Override public ObjectAdapter getResultsAdapter() { return mRowsAdapter; } @Override public boolean onQueryTextChange(String newQuery) { Log.i(TAG, String.format("Search Query Text Change %s", newQuery)); loadQuery(newQuery); return true; } @Override public boolean onQueryTextSubmit(String query) { Log.i(TAG, String.format("Search Query Text Submit %s", query)); loadQuery(query); return true; } private void loadRows() { // offload processing from the UI thread new AsyncTask<String, Void, ListRow>() { private final String query = mQuery; @Override protected void onPreExecute() { mRowsAdapter.clear(); } @Override protected ListRow doInBackground(String... params) { ProviderAggregator.getDefault().startSearch(query); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private boolean hasPermission(final String permission) { final Context context = getActivity(); return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission( permission, context.getPackageName()); } public boolean hasResults() { return mRowsAdapter.size() > 0; } private void loadQuery(String query) { mHandler.removeCallbacks(mDelayedLoad); if (!TextUtils.isEmpty(query) && !query.equals("nil")) { mQuery = query; mHandler.postDelayed(mDelayedLoad, SEARCH_DELAY_MS); } } private class SearchLocalCallback implements ILocalCallback { @Override public void onSongUpdate(List<Song> s) { if (mRowsAdapter != null) { mHandler.post(mUpdateAdapter); } } @Override public void onAlbumUpdate(List<Album> a) { if (mRowsAdapter != null) { mHandler.post(mUpdateAdapter); } } @Override public void onPlaylistUpdate(List<Playlist> p) { if (mRowsAdapter != null) { mHandler.post(mUpdateAdapter); } } @Override public void onPlaylistRemoved(String ref) { } @Override public void onArtistUpdate(List<Artist> a) { if (mRowsAdapter != null) { mHandler.post(mUpdateAdapter); } } @Override public void onProviderConnected(IMusicProvider provider) { } @Override public void onSearchResult(List<SearchResult> searchResult) { final ArrayObjectAdapter artistRowAdapter = new ArrayObjectAdapter(new CardPresenter()); final ArrayObjectAdapter albumRowAdapter = new ArrayObjectAdapter(new CardPresenter()); final ArrayObjectAdapter songsRowAdapter = new ArrayObjectAdapter(new CardPresenter()); final ArrayObjectAdapter playlistsRowAdapter = new ArrayObjectAdapter(new CardPresenter()); final ProviderAggregator aggregator = ProviderAggregator.getDefault(); for (SearchResult result : searchResult) { for (String ref : result.getArtistList()) { Artist artist = aggregator.retrieveArtist(ref, result.getIdentifier()); if (artist != null) { artistRowAdapter.add(artist); } } for (String ref : result.getAlbumsList()) { Album album = aggregator.retrieveAlbum(ref, result.getIdentifier()); if (album != null) { albumRowAdapter.add(album); } } for (String ref : result.getSongsList()) { Song song = aggregator.retrieveSong(ref, result.getIdentifier()); if (song != null) { songsRowAdapter.add(song); } } for (String ref : result.getPlaylistList()) { Playlist playlist = aggregator.retrievePlaylist(ref, result.getIdentifier()); if (playlist != null) { playlistsRowAdapter.add(playlist); } } } mHandler.post(new Runnable() { @Override public void run() { mRowsAdapter.clear(); HeaderItem header = new HeaderItem(getString(R.string.artist)); mRowsAdapter.add(new ListRow(header, artistRowAdapter)); header = new HeaderItem(getString(R.string.albums)); mRowsAdapter.add(new ListRow(header, albumRowAdapter)); header = new HeaderItem(getString(R.string.songs)); mRowsAdapter.add(new ListRow(header, songsRowAdapter)); header = new HeaderItem(getString(R.string.tab_playlists)); mRowsAdapter.add(new ListRow(header, playlistsRowAdapter)); mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size()); } }); } } }