Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android TV: This is not the idiot box you are looking for

Android TV: This is not the idiot box you are looking for

Do you wonder what Android TV is and how could you make the most out of it? Take a look at the features it has to offer and how to build a great user experience with Android TV. Talk given at Droidcon Madrid 2015.

David González

April 26, 2015
Tweet

More Decks by David González

Other Decks in Technology

Transcript

  1. “Designers are also nice people*” Sebastiano Poggi Android Developer @Novoda

    This is not the idiot box you are looking for *allegedly Dave Clements Head of Design @Novoda
  2. Manifest.xml <application android:banner="@drawable/banner">
 <activity
 android:name="com.example.android.TvActivity"
 android:label="@string/app_name"
 android:theme="@style/Theme.Leanback">
 
 <intent-filter>
 <action

    android:name="android.intent.action.MAIN" />
 <category android:name= "android.intent.category.LEANBACK_LAUNCHER" /> 
 </intent-filter>
 
 </activity>
 </application>
  3. There is no touchable screen <ImageView
 android:focusable="true"
 android:focusableInTouchMode="true"
 android:nextFocusDown="@+id/view1"
 android:nextFocusUp="@+id/view2"

    /> 
 KeyEvent.KEYCODE_DPAD_CENTER;
 KeyEvent.KEYCODE_DPAD_LEFT;
 KeyEvent.KEYCODE_DPAD_RIGHT;
 KeyEvent.KEYCODE_DPAD_DOWN;
 KeyEvent.KEYCODE_DPAD_UP ;

  4. There is no touchable screen <ImageView
 android:focusable="true"
 android:focusableInTouchMode="true"
 android:nextFocusDown="@+id/view1"
 android:nextFocusUp="@+id/view2"

    /> 
 KeyEvent.KEYCODE_DPAD_CENTER;
 KeyEvent.KEYCODE_DPAD_LEFT;
 KeyEvent.KEYCODE_DPAD_RIGHT;
 KeyEvent.KEYCODE_DPAD_DOWN;
 KeyEvent.KEYCODE_DPAD_UP ;

  5. Check for a TV Device public static final String TAG

    = "DeviceTypeRuntimeCheck";
 
 UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
 if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { 
 Log.d(TAG, "Running on a TV Device")
 } else {
 Log.d(TAG, "Running on a non-TV Device")
 }
  6. Customising the BrowseFragment public class MainFragment extends BrowseFragment setBadgeDrawable(getDrawable(R.drawable.videos_by_google_banner));
 setTitle(getString(R.string.browse_title));

    
 setHeadersState(HEADERS_ENABLED);
 setHeadersTransitionOnBackEnabled(true); 
 setBrandColor(getResources().getColor(R.color.fastlane_background));
 setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
 

  7. Presenting data setHeaderPresenterSelector(new PresenterSelector() {
 @Override
 public Presenter getPresenter(Object o)

    {
 return new IconHeaderItemPresenter();
 }
 }); public class IconHeaderItemPresenter extends Presenter { @Override
 public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
 return new ViewHolder(inflater.inflate(R.layout.icon_header_item, null));
 } @Override
 public void onBindViewHolder(ViewHolder viewHolder, Object o) {
 View rootView = viewHolder.view;
 } }
  8. Presenting data setHeaderPresenterSelector(new PresenterSelector() {
 @Override
 public Presenter getPresenter(Object o)

    {
 return new IconHeaderItemPresenter();
 }
 }); public class IconHeaderItemPresenter extends Presenter { @Override
 public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
 return new ViewHolder(inflater.inflate(R.layout.icon_header_item, null));
 } @Override
 public void onBindViewHolder(ViewHolder viewHolder, Object o) {
 View rootView = viewHolder.view;
 } }
  9. Presenting data mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
 CardPresenter cardPresenter =

    new CardPresenter(); for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
 List<Movie> list = entry.getValue();
 
 for (int j = 0; j < list.size(); j++) {
 listRowAdapter.add(list.get(j));
 }
 HeaderItem header = new HeaderItem(i, entry.getKey());
 i++;
 mRowsAdapter.add(new ListRow(header, listRowAdapter));
 } setAdapter(mRowsAdapter);
  10. Presenting data mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
 CardPresenter cardPresenter =

    new CardPresenter(); for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
 List<Movie> list = entry.getValue();
 
 for (int j = 0; j < list.size(); j++) {
 listRowAdapter.add(list.get(j));
 }
 HeaderItem header = new HeaderItem(i, entry.getKey());
 i++;
 mRowsAdapter.add(new ListRow(header, listRowAdapter));
 } setAdapter(mRowsAdapter);
  11. Presenting data mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
 CardPresenter cardPresenter =

    new CardPresenter(); for (Map.Entry<String, List<Movie>> entry : data.entrySet()) {
 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
 List<Movie> list = entry.getValue();
 
 for (int j = 0; j < list.size(); j++) {
 listRowAdapter.add(list.get(j));
 }
 HeaderItem header = new HeaderItem(i, entry.getKey());
 i++;
 mRowsAdapter.add(new ListRow(header, listRowAdapter));
 } setAdapter(mRowsAdapter);
  12. Setup event listeners setOnSearchClickedListener(new View.OnClickListener() {
 
 @Override
 public void

    onClick(View view) {
 Intent intent = new Intent(getActivity(), SearchActivity.class);
 startActivity(intent);
 }
 });
 
 setOnItemViewClickedListener(new ItemViewClickedListener());
 setOnItemViewSelectedListener(new ItemViewSelectedListener());
  13. Setup event listeners setOnSearchClickedListener(new View.OnClickListener() {
 
 @Override
 public void

    onClick(View view) {
 Intent intent = new Intent(getActivity(), SearchActivity.class);
 startActivity(intent);
 }
 });
 
 setOnItemViewClickedListener(new ItemViewClickedListener());
 setOnItemViewSelectedListener(new ItemViewSelectedListener());
  14. Setup event listeners private final class ViewSelectedListener implements OnItemViewSelectedListener {


    @Override
 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
 
 mBackgroundURI = ((Movie) item).getBackgroundImageURI();
 startBackgroundTimer(); 
 }
 }
  15. Setting a background image private void prepareBackgroundManager() {
 mBackgroundManager =

    BackgroundManager.getInstance(getActivity());
 mBackgroundManager.attach(getActivity().getWindow());
 } Glide.with(getActivity())
 .load(uri)
 .centerCrop()
 .error(mDefaultBackground)
 .into(new SimpleTarget<GlideDrawable>(width, height) {
 @Override
 public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
 mBackgroundManager.setDrawable(resource);
 }
 });
  16. Setting a background image private void prepareBackgroundManager() {
 mBackgroundManager =

    BackgroundManager.getInstance(getActivity());
 mBackgroundManager.attach(getActivity().getWindow());
 } Glide.with(getActivity())
 .load(uri)
 .centerCrop()
 .error(mDefaultBackground)
 .into(new SimpleTarget<GlideDrawable>(width, height) {
 @Override
 public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
 mBackgroundManager.setDrawable(resource);
 }
 });
  17. Presenting details /*
 * LeanbackDetailsFragment extends DetailsFragment, a Wrapper fragment

    for leanback details screens.
 * It shows a detailed view of video and its meta plus related videos.
 */
 public class MovieDetailsFragment extends android.support.v17.leanback.app.DetailsFragment {
  18. Presenting details private void setupAdapter() {
 mPresenterSelector = new ClassPresenterSelector();


    mAdapter = new ArrayObjectAdapter(mPresenterSelector);
 setAdapter(mAdapter);
 } private void setupDetailsOverviewRowPresenter() { DetailsOverviewRowPresenter detailsPresenter =
 new DetailsOverviewRowPresenter( new DetailsDescriptionPresenter());
 detailsPresenter.setBackgroundColor(R.color.selected_background);
 detailsPresenter.setStyleLarge(true); mPresenterSelector.addClassPresenter(DetailsOverviewRow.class, detailsPresenter); }
  19. Presenting details private void setupAdapter() {
 mPresenterSelector = new ClassPresenterSelector();


    mAdapter = new ArrayObjectAdapter(mPresenterSelector);
 setAdapter(mAdapter);
 } private void setupDetailsOverviewRowPresenter() { DetailsOverviewRowPresenter detailsPresenter =
 new DetailsOverviewRowPresenter( new DetailsDescriptionPresenter());
 detailsPresenter.setBackgroundColor(R.color.selected_background);
 detailsPresenter.setStyleLarge(true); mPresenterSelector.addClassPresenter(DetailsOverviewRow.class, detailsPresenter); }
  20. Presenting details 
 public class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter {
 


    @Override
 protected void onBindDescription(ViewHolder viewHolder, Object item) {
 Movie movie = (Movie) item;
 
 if (movie != null) {
 viewHolder.getTitle().setText(movie.getTitle());
 viewHolder.getSubtitle().setText(movie.getStudio());
 viewHolder.getBody().setText(movie.getDescription());
 }
 }
 }
  21. Adding actions private void setupDetailsOverviewRow() {
 final DetailsOverviewRow row =

    new DetailsOverviewRow(mSelectedMovie);
 row.addAction(new Action(ACTION_WATCH_TRAILER, 
 getResources().getString(
 R.string.watch_trailer_1), 
 getResources().getString(R.string.watch_trailer_2))); 
 row.addAction(new Action(ACTION_RENT, 
 getResources().getString(R.string.rent_1),
 getResources().getString(R.string.rent_2))); 
 row.addAction(new Action(ACTION_BUY, 
 getResources().getString(R.string.buy_1),
 getResources().getString(R.string.buy_2))); 
 mAdapter.add(row);
 }
  22. Adding actions private void setupDetailsOverviewRow() {
 final DetailsOverviewRow row =

    new DetailsOverviewRow(mSelectedMovie);
 row.addAction(new Action(ACTION_WATCH_TRAILER, 
 getResources().getString(
 R.string.watch_trailer_1), 
 getResources().getString(R.string.watch_trailer_2))); 
 row.addAction(new Action(ACTION_RENT, 
 getResources().getString(R.string.rent_1),
 getResources().getString(R.string.rent_2))); 
 row.addAction(new Action(ACTION_BUY, 
 getResources().getString(R.string.buy_1),
 getResources().getString(R.string.buy_2))); 
 mAdapter.add(row);
 }
  23. Adding actions private void setupDetailsOverviewRow() {
 final DetailsOverviewRow row =

    new DetailsOverviewRow(mSelectedMovie);
 row.addAction(new Action(ACTION_WATCH_TRAILER, 
 getResources().getString(
 R.string.watch_trailer_1), 
 getResources().getString(R.string.watch_trailer_2))); 
 row.addAction(new Action(ACTION_RENT, 
 getResources().getString(R.string.rent_1),
 getResources().getString(R.string.rent_2))); 
 row.addAction(new Action(ACTION_BUY, 
 getResources().getString(R.string.buy_1),
 getResources().getString(R.string.buy_2))); 
 mAdapter.add(row);
 }
  24. Add a PlaybackOverlayFragment <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent" >
 
 <VideoView

    android:id="@+id/videoView"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 </VideoView>
 
 <fragment
 android:id="@+id/playback_controls_fragment"
 android:name="ui.PlaybackOverlayFragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />
 
 </FrameLayout>
  25. Playing a video in the background /*
 * Class for

    video playback with media control
 */
 public class PlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment
  26. Playing a video in the background PlaybackControlsRowPresenter playbackControlsRowPresenter;
 playbackControlsRowPresenter =

    new PlaybackControlsRowPresenter(
 new DescriptionPresenter()); ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); mRowsAdapter = new ArrayObjectAdapter(ps); setAdapter(mRowsAdapter); static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter {
 @Override
 protected void onBindDescription(ViewHolder viewHolder, Object item) {
 viewHolder.getTitle().setText(((Movie) item).getTitle());
 viewHolder.getSubtitle().setText(((Movie) item).getStudio());
 }
 }

  27. Playing a video in the background PlaybackControlsRowPresenter playbackControlsRowPresenter;
 playbackControlsRowPresenter =

    new PlaybackControlsRowPresenter(
 new DescriptionPresenter()); ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); mRowsAdapter = new ArrayObjectAdapter(ps); setAdapter(mRowsAdapter); static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter {
 @Override
 protected void onBindDescription(ViewHolder viewHolder, Object item) {
 viewHolder.getTitle().setText(((Movie) item).getTitle());
 viewHolder.getSubtitle().setText(((Movie) item).getStudio());
 }
 }

  28. Playback Controls private void addPlaybackControlsRow() { ControlButtonPresenterSelector presenterSelector = new

    ControlButtonPresenterSelector();
 mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
 mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); 
 mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
 mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); mPlayPauseAction = new PlayPauseAction(sContext);
 mRepeatAction = new RepeatAction(sContext); mPrimaryActionsAdapter.add(mPlayPauseAction); mSecondaryActionsAdapter.add(mRepeatAction); }
  29. Playback Controls private void addPlaybackControlsRow() { ControlButtonPresenterSelector presenterSelector = new

    ControlButtonPresenterSelector();
 mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
 mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); 
 mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
 mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); mPlayPauseAction = new PlayPauseAction(sContext);
 mRepeatAction = new RepeatAction(sContext); mPrimaryActionsAdapter.add(mPlayPauseAction); mSecondaryActionsAdapter.add(mRepeatAction); }
  30. Request behind playback @Override
 public void onPause() {
 super.onPause();
 requestVisibleBehind(true);


    } @Override
 public void onVisibleBehindCanceled() {
 super.onVisibleBehindCanceled();
 stopPlayback();
 } @Override
 protected void onStop() {
 super.onStop();
 mSession.release();
 }
  31. Add Service to the Manifest <service
 android:name=“.recommendations.notifications.UpdateRecommendationsService"
 android:enabled="true" />
 


    <receiver
 android:name=“.recommendations.scheduler.RecommendationsBootupService"
 android:enabled="true"
 android:exported="false">
 <intent-filter>
 <action android:name="android.intent.action.BOOT_COMPLETED" />
 </intent-filter>
 </receiver>
  32. Create the notification Bundle extras = new Bundle();
 extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI, imageUri);


    NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
 .setContentTitle(title)
 .setContentText(description)
 .setPriority(PRIORITY)
 .setLocalOnly(true)
 .setOngoing(true)
 .setColor(CARD_TEXT_BACKGROUND_COLOR_RESOURCE)
 .setCategory(Notification.CATEGORY_RECOMMENDATION)
 .setLargeIcon(filmPoster)
 .setSmallIcon(CARD_SMALL_APPLICATION_LOGO)
 .setContentIntent(intent)
 .setExtras(extras);

  33. And trigger it 
 Notification notification = new NotificationCompat.BigPictureStyle(builder).build();
 


    NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 notificationManager.notify(notificationUniqueId, notification);
  34. Setup the Media Session mSession = new MediaSession (this, "MyApp");


    mSession.setCallback(new MediaSessionCallback());
 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
 MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); if (!mSession.isActive()) {
 mSession.setActive(true);
 }
  35. Use the Media Session private void updatePlaybackState() {
 long position

    = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
 position = mMediaPlayer.getCurrentPosition();
 
 PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
 .setActions(getAvailableActions());
 stateBuilder.setState(mState, position, 1.0f);
 mSession.setPlaybackState(stateBuilder.build());
 }
  36. Use the Media Session private void updateMetadata(MediaData myData) {
 MediaMetadata.Builder

    metadataBuilder = new MediaMetadata.Builder();
 metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE,
 myData.displayTitle);
 metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE,
 myData.displaySubtitle);
 metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI,
 myData.artUri);
 mSession.setMetadata(metadataBuilder.build());
 }
  37. Because everybody loves Content Providers private static UriMatcher buildUriMatcher() {


    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
 matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
 matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
 return matcher;
 } @Override
 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
 switch (URI_MATCHER.match(uri)) {
 case SEARCH_SUGGEST:
 return getSuggestions(selectionArgs[0]);
 default:
 throw new IllegalArgumentException("Unknown Uri: " + uri);
 }
 }
  38. Because everybody loves Content Providers private static UriMatcher buildUriMatcher() {


    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
 matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
 matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
 return matcher;
 } @Override
 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
 switch (URI_MATCHER.match(uri)) {
 case SEARCH_SUGGEST:
 return getSuggestions(selectionArgs[0]);
 default:
 throw new IllegalArgumentException("Unknown Uri: " + uri);
 }
 }
  39. Provide Cursor with your own search results private void addFilmToCursor(Film

    film, MatrixCursor matrixCursor) {
 matrixCursor.addRow(
 new Object[]{
 film.getId().toString(),
 film.getTitle(),
 getDirectorsLabel(film),
 VIDEO_MP4,
 film.getYear(),
 TimeUnit.MINUTES.toMillis(film.getDuration()),
 film.getPosterUrl(),
 film.getId().toString(),
 MubiIntentAction.SEARCH.getAction()
 }
 );
 }
  40. ExoPlayer <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/root"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:keepScreenOn="true">
 
 <com.google.android.exoplayer.VideoSurfaceView android:id="@+id/surface_view"


    android:layout_width="match_parent"
 android:layout_height="match_parent"/>
 
 <fragment
 android:id="@+id/playback_controls_fragment" android:name="fastlane.PlaybackOverlayFragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent" />
 
 </FrameLayout>
  41. Prepare ExoPlayer private void preparePlayer() { 
 SampleSource sampleSource =


    new FrameworkSampleSource(this, Uri.parse(mVideo.getContentUrl()), null, RENDERER_COUNT);
 
 MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); 
 TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
 
 }
  42. Prepare ExoPlayer private void preparePlayer() { 
 SampleSource sampleSource =


    new FrameworkSampleSource(this, Uri.parse(mVideo.getContentUrl()), null, RENDERER_COUNT);
 
 MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); 
 TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
 
 }
  43. Prepare ExoPlayer private void preparePlayer() { 
 SampleSource sampleSource =


    new FrameworkSampleSource(this, Uri.parse(mVideo.getContentUrl()), null, RENDERER_COUNT);
 
 MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); 
 TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);
 
 }
  44. Setup the player and start player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);


    player.addListener(this);
 player.prepare(videoRenderer, audioRenderer); Surface surface = surfaceView.getHolder().getSurface();
 player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); playerControl = new PlayerControl(player);
 playerControl.start();
  45. Setup the player and start player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);


    player.addListener(this);
 player.prepare(videoRenderer, audioRenderer); Surface surface = surfaceView.getHolder().getSurface();
 player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); playerControl = new PlayerControl(player);
 playerControl.start();
  46. Setup the player and start player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);


    player.addListener(this);
 player.prepare(videoRenderer, audioRenderer); Surface surface = surfaceView.getHolder().getSurface();
 player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); playerControl = new PlayerControl(player);
 playerControl.start();