/* * Copyright (C) 2014 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.app.Activity; import android.app.FragmentManager; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.support.v17.leanback.app.BrowseFragment; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; 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.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.v4.app.ActivityOptionsCompat; import android.support.v7.graphics.Palette; import android.view.View; import com.fastbootmobile.encore.api.common.Pair; import com.fastbootmobile.encore.app.R; import com.fastbootmobile.encore.app.adapters.HistoryAdapter; import com.fastbootmobile.encore.framework.ListenLogger; import com.fastbootmobile.encore.framework.PluginsLookup; 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 com.fastbootmobile.encore.providers.ProviderConnection; import com.fastbootmobile.encore.providers.ProviderIdentifier; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; public class TvActivity extends Activity { public static final String TAG = "TvActivity"; private static final int MSG_NOTIFY_CHANGES = 1; private static final int TYPE_ALBUM = 0; private static final int TYPE_ARTIST = 1; private static final int ROW_RECENTS = 0; private static final int ROW_RECOMMENDATIONS = 1; private static final int ROW_LIBRARY = 2; private static final int ROW_PLAYLISTS = 3; private static final int ROW_SETTINGS = 4; private ArrayObjectAdapter mRowsAdapter; private Handler mHandler; private int mNumSuggestions; protected BrowseFragment mBrowseFragment; private ILocalCallback mLocalCallback = new TvLocalCallback(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.tv_browsefragment); mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_NOTIFY_CHANGES: if (mRowsAdapter != null) { mRowsAdapter.notifyArrayItemRangeChanged(0, 2); } break; } } }; final FragmentManager fragmentManager = getFragmentManager(); mBrowseFragment = (BrowseFragment) fragmentManager.findFragmentById( R.id.browse_fragment); // Set display parameters for the BrowseFragment mBrowseFragment.setHeadersState(BrowseFragment.HEADERS_ENABLED); mBrowseFragment.setTitle(getString(R.string.app_name)); mBrowseFragment.setBadgeDrawable(getResources().getDrawable( R.mipmap.ic_launcher)); // Set search interface mBrowseFragment.setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(TvActivity.this, TvSearchActivity.class); startActivity(intent); } }); mBrowseFragment.setSearchAffordanceColor(getResources().getColor(R.color.primary_dark)); // Setup event listeners mBrowseFragment.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(TvActivity.this, TvAlbumDetailsActivity.class); intent.putExtra(TvAlbumDetailsActivity.EXTRA_ALBUM, album); intent.putExtra(TvAlbumDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( TvActivity.this, ((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(TvActivity.this, TvArtistDetailsActivity.class); intent.putExtra(TvArtistDetailsActivity.EXTRA_ARTIST, artist); intent.putExtra(TvArtistDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( TvActivity.this, ((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(TvActivity.this, TvPlaylistDetailsActivity.class); intent.putExtra(TvPlaylistDetailsActivity.EXTRA_PLAYLIST, playlist); intent.putExtra(TvPlaylistDetailsActivity.EXTRA_COLOR, color); Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( TvActivity.this, ((ImageCardView) itemViewHolder.view).getMainImageView(), TvAlbumDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); startActivity(intent, bundle); } else if (item instanceof SettingsItem) { SettingsItem sitem = (SettingsItem) item; switch (sitem.getType()) { case SettingsItem.ITEM_PROVIDERS: startActivity(new Intent(TvActivity.this, TvProvidersActivity.class)); break; case SettingsItem.ITEM_EFFECTS: Intent intent = new Intent(TvActivity.this, TvProvidersActivity.class); intent.putExtra(TvProvidersActivity.EXTRA_DSP_MODE, true); startActivity(intent); break; } } else if (item instanceof MyLibraryItem) { MyLibraryItem libraryItem = (MyLibraryItem) item; Intent intent = new Intent(TvActivity.this, TvEntityGridActivity.class); switch (libraryItem.getType()) { case MyLibraryItem.TYPE_ALBUMS: intent.putExtra(TvEntityGridActivity.EXTRA_MODE, TvEntityGridActivity.MODE_ALBUM); break; case MyLibraryItem.TYPE_ARTISTS: intent.putExtra(TvEntityGridActivity.EXTRA_MODE, TvEntityGridActivity.MODE_ARTIST); break; } startActivity(intent); } } }); // Build adapter items mHandler.postDelayed(new Runnable() { public void run() { buildRowsAdapter(); } }, 1500); } @Override protected void onResume() { super.onResume(); ProviderAggregator.getDefault().addUpdateCallback(mLocalCallback); if (mNumSuggestions > 2) { requestAdapterUpdate(); } else { mHandler.postDelayed(new Runnable() { public void run() { buildRowsAdapter(); } }, 1500); } } @Override protected void onPause() { super.onPause(); ProviderAggregator.getDefault().removeUpdateCallback(mLocalCallback); } private void requestAdapterUpdate() { mHandler.removeMessages(MSG_NOTIFY_CHANGES); mHandler.sendEmptyMessage(MSG_NOTIFY_CHANGES); } private void buildRowsAdapter() { ClassPresenterSelector selector = new ClassPresenterSelector(); selector.addClassPresenter(ListRow.class, new ListRowPresenter()); selector.addClassPresenter(ShadowlessListRow.class, ShadowlessListRow.createPresenter(this)); mRowsAdapter = new ArrayObjectAdapter(selector); // Generate rows contents generateRecentlyPlayedRow(); generateRecommendations(); generateMyLibraryRow(); generatePlaylistsRow(); generateSettingsRow(); mBrowseFragment.setAdapter(mRowsAdapter); } private void generateRecentlyPlayedRow() { final Random rand = new Random(); final ProviderAggregator aggregator = ProviderAggregator.getDefault(); // First row: Recently played (10 items, randomly artist or album) ListenLogger logger = new ListenLogger(this); List<ListenLogger.LogEntry> logEntries = HistoryAdapter.sortByTime(logger.getEntries(25)); ArrayObjectAdapter logEntriesRowAdapter = new ArrayObjectAdapter(new CardPresenter()); int entriesCount = 0; for (ListenLogger.LogEntry logEntry : logEntries) { if (entriesCount == 10) break; // Get the song information Song song = aggregator.retrieveSong(logEntry.getReference(), logEntry.getIdentifier()); if (song == null) { // That song might not exist anymore, keep moving continue; } final int type = rand.nextInt(2); if (type == TYPE_ALBUM && song.getAlbum() == null || type == TYPE_ARTIST && song.getArtist() == null) { // We don't have the requested track info, so skip it continue; } if (type == TYPE_ALBUM) { Album album = aggregator.retrieveAlbum(song.getAlbum(), song.getProvider()); logEntriesRowAdapter.add(album); } else if (type == TYPE_ARTIST) { Artist artist = aggregator.retrieveArtist(song.getArtist(), song.getProvider()); logEntriesRowAdapter.add(artist); } ++entriesCount; } // Build Recently Played Leanback item HeaderItem header = new HeaderItem(ROW_RECENTS, "Recently played"); ListRow row = new ListRow(header, logEntriesRowAdapter); if (mRowsAdapter.size() > ROW_RECENTS) { mRowsAdapter.replace(ROW_RECENTS, row); } else { mRowsAdapter.add(row); } } private void generateRecommendations() { final Random rand = new Random(); final ProviderAggregator aggregator = ProviderAggregator.getDefault(); // Get all the available tracks to build Recommendations final List<Pair<String, ProviderIdentifier>> availableReferences = new ArrayList<>(); final List<ProviderConnection> providers = PluginsLookup.getDefault().getAvailableProviders(); final List<Playlist> playlists = aggregator.getAllPlaylists(); for (Playlist p : playlists) { Iterator<String> it = p.songs(); while (it.hasNext()) { String ref = it.next(); Pair<String, ProviderIdentifier> pair = Pair.create(ref, p.getProvider()); if (!availableReferences.contains(pair)) { availableReferences.add(pair); } } } for (ProviderConnection provider : providers) { IMusicProvider binder = provider.getBinder(); if (binder != null) { int limit = 50; int offset = 0; boolean goAhead = true; try { while (goAhead) { List<Song> songs = binder.getSongs(offset, limit); if (songs == null) { goAhead = false; continue; } if (songs.size() < limit) { goAhead = false; } offset += songs.size(); for (Song song : songs) { Pair<String, ProviderIdentifier> pair = Pair.create(song.getRef(), song.getProvider()); if (!availableReferences.contains(pair)) { availableReferences.add(pair); } } } } catch (RemoteException ignore) { } } } // Randomly generate recommendations ArrayObjectAdapter recommendedRowAdapter = new ArrayObjectAdapter(new CardPresenter()); int entriesCount = 0; List<String> knownRefs = new ArrayList<>(); for (Pair<String, ProviderIdentifier> ref : availableReferences) { if (entriesCount == 20) break; // Get the song information Song song = aggregator.retrieveSong(ref.first, ref.second); if (song == null) { // That song might not exist anymore, keep moving continue; } final int type = rand.nextInt(2); if (type == TYPE_ALBUM && (song.getAlbum() == null || knownRefs.contains(song.getAlbum())) || type == TYPE_ARTIST && (song.getArtist() == null || knownRefs.contains(song.getArtist()))) { // We don't have the requested track info, so skip it continue; } if (type == TYPE_ALBUM) { knownRefs.add(song.getAlbum()); Album album = aggregator.retrieveAlbum(song.getAlbum(), song.getProvider()); recommendedRowAdapter.add(album); } else if (type == TYPE_ARTIST) { knownRefs.add(song.getArtist()); Artist artist = aggregator.retrieveArtist(song.getArtist(), song.getProvider()); recommendedRowAdapter.add(artist); } ++entriesCount; } mNumSuggestions = recommendedRowAdapter.size(); // Build Recommended Leanback item HeaderItem header = new HeaderItem(ROW_RECOMMENDATIONS, "Recommended for you"); ListRow row = new ListRow(header, recommendedRowAdapter); if (mRowsAdapter.size() > ROW_RECOMMENDATIONS) { mRowsAdapter.replace(ROW_RECOMMENDATIONS, row); } else { mRowsAdapter.add(row); } } private void generateMyLibraryRow() { // Build My Library item ArrayObjectAdapter libraryAdapter = new ArrayObjectAdapter(new CardPresenter()); libraryAdapter.add(new MyLibraryItem(MyLibraryItem.TYPE_ARTISTS)); libraryAdapter.add(new MyLibraryItem(MyLibraryItem.TYPE_ALBUMS)); HeaderItem header = new HeaderItem(ROW_LIBRARY, getString(R.string.title_section_my_songs)); ListRow row = new ListRow(header, libraryAdapter); if (mRowsAdapter.size() > ROW_LIBRARY) { mRowsAdapter.replace(ROW_LIBRARY, row); } else { mRowsAdapter.add(row); } } private void generatePlaylistsRow() { // Build Playlists items final ProviderAggregator aggregator = ProviderAggregator.getDefault(); final List<Pair<String, ProviderIdentifier>> availableReferences = new ArrayList<>(); final List<Playlist> playlists = aggregator.getAllPlaylists(); for (Playlist p : playlists) { Iterator<String> it = p.songs(); while (it.hasNext()) { String ref = it.next(); Pair<String, ProviderIdentifier> pair = Pair.create(ref, p.getProvider()); if (!availableReferences.contains(pair)) { availableReferences.add(pair); } } } ArrayObjectAdapter playlistsAdapter = new ArrayObjectAdapter(new CardPresenter()); playlistsAdapter.addAll(0, playlists); HeaderItem header = new HeaderItem(ROW_PLAYLISTS, getString(R.string.title_section_playlists)); ListRow row = new ListRow(header, playlistsAdapter); if (mRowsAdapter.size() > ROW_PLAYLISTS) { mRowsAdapter.replace(ROW_PLAYLISTS, row); } else { mRowsAdapter.add(row); } } private void generateSettingsRow() { // Build Settings items ArrayObjectAdapter settingsAdapter = new ArrayObjectAdapter(new IconPresenter()); settingsAdapter.add(new SettingsItem(SettingsItem.ITEM_PROVIDERS)); settingsAdapter.add(new SettingsItem(SettingsItem.ITEM_EFFECTS)); settingsAdapter.add(new SettingsItem(SettingsItem.ITEM_LICENSES)); HeaderItem header = new HeaderItem(ROW_SETTINGS, getString(R.string.title_activity_settings)); ShadowlessListRow row = new ShadowlessListRow(header, settingsAdapter); if (mRowsAdapter.size() > ROW_SETTINGS) { mRowsAdapter.replace(ROW_SETTINGS, row); } else { mRowsAdapter.add(row); } } private class TvLocalCallback implements ILocalCallback { @Override public void onSongUpdate(List<Song> s) { if (mRowsAdapter != null) { if (mNumSuggestions < 2) { generateRecommendations(); } else { requestAdapterUpdate(); } } } @Override public void onAlbumUpdate(List<Album> a) { if (mRowsAdapter != null) { requestAdapterUpdate(); } } @Override public void onPlaylistUpdate(List<Playlist> p) { if (mRowsAdapter != null) { generatePlaylistsRow(); } } @Override public void onPlaylistRemoved(String ref) { } @Override public void onArtistUpdate(List<Artist> a) { if (mRowsAdapter != null) { requestAdapterUpdate(); } } @Override public void onProviderConnected(IMusicProvider provider) { if (mRowsAdapter != null) { requestAdapterUpdate(); } } @Override public void onSearchResult(List<SearchResult> searchResult) { } } }