Developing for TV

D8a3623b157508fecdae1f8e756f362f?s=47 cmota
March 06, 2015

Developing for TV

The concept of a single device has been deprecated for years now. We have a smartphone on our pockets, a smart watch on our wrists, a laptop on our desks, a tablet on our backpacks, a TV on our living rooms, etc. and although we have this diversity of devices only recently they learn about the existence of each other and start to interact. Ideas like receiving a call on our phones and answer it instead on the computer or using our devices as remote controls for a TV game are now possible.

Over the last years our televisions have been losing our attention. They became difficult to use - we no longer have a small number of channels or 9 to 5 schedules which allowed us to watch a specific movie at a defined time - for example we want to watch a movie after a long work day without pressing dozens of buttons and without watching through long commercials. We want the same experience that we have on our computers or tablets. Manufacturers have been attempting to solve this problem by filling our TV’s with a large amount of applications that we might have opened once to see how they work - and well… they are still there… somewhere.

During the latest Google I/O, Google presented what they believe it will be next TV experience - Google TV. Their proposal is an android set top box along with a new support library - leanback - as well as a defined set of designing rules which will allow the user to to take the best experience from their TV’s. Moreover, you can use your mobile phone as a secondary device/or remote control increasing the number of features available and providing new ways to interact with your TV.

On this talk I’m planning to give an overview of what is possible to do with this new system along with some code samples - what to do, why to do it and how can I do it, examples and pitfalls to avoid.

D8a3623b157508fecdae1f8e756f362f?s=128

cmota

March 06, 2015
Tweet

Transcript

  1. 2.

    C a r l o s M o t a

    2 @cafonsomota cafonsomota@gmail.com
  2. 3.
  3. 7.
  4. 12.

    12 *2014 Q3 study available at http://www.marketingcharts.com/television/are-young-people-watching-less-tv-24817/ • Average viewing

    141h 19 mins/per month • >5B hours watched daily worldwide • Declined TV viewing among 12-64 age groups • Increased usage of smartphones for watching video TV
  5. 14.

    14

  6. 20.

    20 compileSdkVersion  21 • Distance to the TV • Larger

    (much larger) screens • Always connected to the internet • There’s no screen rotation (always landscape) • Not touchable • Use of remote control
  7. 24.
  8. 26.

         <RelativeLayout              

     android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_gravity="center"                android:focusable="true"                android:focusableInTouchMode="true">                <ProgressBar                        android:id=“@+id/pb_spinner_action_ingoing"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:indeterminateDrawable="@drawable/progress_spinner"                        android:visibility="gone"  />                <ImageView                        android:id=“@+id/iv_action_1"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:background=“@drawable/action_1_button”                        android:nextFocusDown=“@+id/iv_action_1"                        android:nextFocusUp=“@+id/btn_search"                        android:nextFocusRight=“@+id/lv_items”                        android:nextFocusLeft=“@+id/ll_tab_nav”                        android:duplicateParentState="true"/>        </RelativeLayout>   example.xml define how D-Pad navigation should work components most be focusable (default = true)
  9. 29.

    29 compile  fileTree(dir:  ‘libs’,  include:  [‘*.jar’]) ListView Text View Linear

    Layout Horizontal Scroll View Linear Layout Image View Text View Image View Linear Layout
  10. 30.

    30 compile  fileTree(dir:  ‘libs’,  include:  [‘*.jar’]) ListView Text View Linear

    Layout Horizontal Scroll View Linear Layout Image View Text View Image View Linear Layout Fragment A Fragment B
  11. 31.

    31 compile  fileTree(dir:  ‘libs’,  include:  [‘*.jar’]) ListView Text View Linear

    Layout Horizontal Scroll View Linear Layout Image View Text View Image View Linear Layout Fragment A Fragment B Handle D-Pad events Handle animations Handle transitions
  12. 35.

    35 compile  ‘com.android.support.leanback-­‐v17:+’ • TV-optimized experience • Defined architecture model

    (MVP) • Pre-built fragments for TV (minSDK=17) • Consistency across different android TV’s • Fast to develop • Higher Performance • Built-In (beautifully) animation library
  13. 36.

         <uses-­‐feature  android:name="android.software.leanback"            

       android:required="true"  />                <uses-­‐feature  android:name="android.hardware.touchscreen"                android:required="false"  />        <application  android:allowBackup="true"                android:label="@string/app_name"                android:banner="@drawable/mdevcon_banner"                android:theme="@style/Theme.Leanback">                <activity                        android:name=".ui.MainActivity"                        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>   AndroidManifest.xml identifies app has been built for Android TV standard theme for TV’s app launcher (equivalent to android:icon) to be defined as an Android TV app
  14. 38.

    38 compile  ‘com.android.support.leanback-­‐v17:+’ Model Presenter View { "mdevcon" : [{

    "category" : "Speakers", "speakers" : [ { "description" : "Carlos is a strong believer that the answer to life, the universe and everything is 42…”, "card" : “avatar_cmota.jpg", "name" : “Carlos Mota", "company" : “WIT Software"}, …
  15. 39.

    39 compile  ‘com.android.support.leanback-­‐v17:+’ • Online • Local data • new

    CardPresenter() • new StringPresenter() • lb_image_card_view.xml • card_view_event.xml Model Presenter View { "mdevcon" : [{ "category" : "Speakers", "speakers" : [ { "description" : "Carlos is a strong believer that the answer to life, the universe and everything is 42…”, "card" : “avatar_cmota.jpg", "name" : “Carlos Mota", "company" : “WIT Software"}, …
  16. 40.

    40 compile  ‘com.android.support.leanback-­‐v17:+’ • BackgroudManager • BrowseFragment • DetailsFragment •

    ErrorFragment • HeadersFragment • PlaybackOverlayFragment • RowsFragment • SearchFragment • VerticalGridFragment • …
  17. 42.

    42 <fragment  android:name=“…BrowseFragment”/> • Multi-pane layout • Standard navigational design

    • Polished transitions between views • Quickly browse across different content
  18. 45.

    public  class  MainBrowseFragment  extends  BrowseFragment  {   !   ...

               @Override        public  void  onActivityCreated(Bundle  savedInstanceState)  {   !      mRowsAdapter  =  new  ArrayObjectAdapter(new  ListRowPresenter());   !      ...            /*  Speakers  */            HeaderItem  headerSpeakers  =  new  HeaderItem(getString(R.string.header_speakers),null);   !          ArrayObjectAdapter  listRowSpeakerAdapter  =  new  ArrayObjectAdapter(new                                              CardPresenter(CARD_WIDTH,  CARD_HEIGHT));   !          for(Speaker  speaker  :  Data.getInstance().getSpeakersList())  {                    listRowSpeakerAdapter.add(speaker);            }   !          mRowsAdapter.add(new  ListRow(headerEventInfo,      listRowEventAdapter));            mRowsAdapter.add(new  ListRow(headerSponsorInfo,  listRowSponsorAdapter));            mRowsAdapter.add(new  ListRow(headerSpeakers,        listRowSpeakerAdapter));            mRowsAdapter.add(new  ListRow(headerPrevious,        listRowPreviousAdapter));   !          ...   !          setAdapter(mRowsAdapter); MainBrowseFragment.java custom presenter category name
  19. 47.

    public  class  CardPresenter  extends  Presenter  {     ...  

    !        @Override          public  ViewHolder  onCreateViewHolder(final  ViewGroup  parent)  {                  ImageCardView  cardView  =  new  ImageCardView(parent.getContext())  {                          @Override                          public  void  setSelected(boolean  selected)  {                                  int  nBackground  =  parent.getContext().getResources().getColor(R.color.bg_normal);                                  int  sBackground  =  parent.getContext().getResources().getColor(R.color.bg_selected);                                  findViewById(R.id.info_field).setBackgroundColor(selected  ?  sBackground  :  nBackground);                                  super.setSelected(selected);                          }                  };   !                cardView.setFocusable(true);                  cardView.setFocusableInTouchMode(true);                  return  new  ViewHolder(cardView);          }   !        @Override          public  void  onBindViewHolder(ViewHolder  viewHolder,  Object  item)  {                  ImageCardView  cardView  =  (ImageCardView)  viewHolder.view;                  cardView.setTitleText(item.getSpeakerName());                  cardView.setContentText(item.getSpeakerCompany());                  cardView.setMainImageDimensions(CARD_WIDTH,  CARD_HEIGHT);                  cardView.setMainImage(viewHolder.view.getResources().getDrawable(item.getSpeakerImage()));          }   !        @Override          public  void  onUnbindViewHolder(ViewHolder  viewHolder)  {                  ((ImageCardView)  viewHolder.view).setMainImage(null);          }   ! CardPresenter.java required for D-Pad focus remove image refs. for GC alternatively we could inflate our custom one
  20. 49.

    public  class  MainBrowseFragment  extends  BrowseFragment  {   !   ...

             @Override          public  void  onActivityCreated(Bundle  savedInstanceState)  {        ...                  setOnItemViewSelectedListener(new  ItemViewSelectedListener());          }   !        private  final  class  ItemViewSelectedListener  implements  OnItemViewSelectedListener  {                                    @Override                  public  void  onItemSelected(Presenter.ViewHolder  viewHolder,  Object  item,                           RowPresenter.rowViewHolder  rvH,  Row  row)  {   !                        if  (item  instanceof  Movie)  {                                  mBackgroundRefId  =  ((Movie)  item).getMovieImage();                          }  else  {                                  mBackgroundRefId  =  0;                                  Log.d(TAG,  "Default  instance  type:  "  +  item);                          }                          startBackgroundTimer();                  }          }   ! MainBrowseFragment.java change background according to selected movie DPAD_RIGHT, DPAD_LEFT, DPAD_DOWN, DPAD_UP
  21. 50.

         private  void  startBackgroundTimer()  {        

           if  (mBackgroundTimer  !=  null)  {                        mBackgroundTimer.cancel();                        mBackgroundTimer  =  null;                }                mBackgroundTimer  =  new  Timer();                mBackgroundTimer.schedule(new  UpdateBackgroundTask(),  BACKGROUND_UPDATE_DELAY);        }        private  void  setBackground()  {                BackgroundManager  backgroundManager  =  BackgroundManager.getInstance(getActivity());                backgroundManager.attach(getActivity().getWindow());                if(mBackgroundRefId  >  0)  {                        backgroundManager.setDrawable(getResources().getDrawable(mBackgroundRefId));                }  else  {                        ColorDrawable  colorDrawable  =  new  ColorDrawable(getResources().getColor(R.color.bg_default));                        backgroundManager.setDrawable(colorDrawable);                }        }        private  class  UpdateBackgroundTask  extends  TimerTask  {                @Override                public  void  run()  {                        mHandler.post(new  Runnable()  {                                @Override                                public  void  run()  {                                        setBackground();                                        mBackgroundTimer.cancel();                                }                        });                }        }   MainBrowseFragment.java time between refreshes
  22. 54.

         private  void  setUIComponentsForMovie(final  Movie  currMovie)  {    

             DetailsOverviewRowPresenter  rowPresenter  =  new  DetailsOverviewRowPresenter(new  DetailsPresenter());              rowPresenter.setOnActionClickedListener(new  OnActionClickedListener()  {                      @Override                      public  void  onActionClicked(Action  action)  {                              if(action.getId()  ==  ACTION_WATCH_TRAILER)  {                                      Intent  intent  =  new  Intent(getActivity(),  PlaybackActivity.class);                                      intent.putExtra(Movie.class.getSimpleName(),  currMovie);                                      startActivity(intent);                              }                      }              });              ClassPresenterSelector  selector  =  new  ClassPresenterSelector();              selector.addClassPresenter(DetailsOverviewRow.class,  rowPresenter);              selector.addClassPresenter(ListRow.class,  new  ListRowPresenter());              DetailsOverviewRow  detailsOverviewRow  =  new  DetailsOverviewRow(currMovie);              detailsOverviewRow.setImageDrawable(getResources().getDrawable(currMovie.getMovieImage()));              detailsOverviewRow.addAction(new  Action(ACTION_WATCH_TRAILER,  currMovie.getActionName()));              mRowsAdapter  =  new  ArrayObjectAdapter(selector);              mRowsAdapter.add(detailsOverviewRow);              ArrayObjectAdapter  listRowAdapter  =  new  ArrayObjectAdapter(new  CardPresenter());              for(Movie  movie  :  movieList)  {                      listRowAdapter.add(movie);              }              HeaderItem  header  =  new  HeaderItem(0,  getString(R.string.details_related),  null);              mRowsAdapter.add(new  ListRow(header,  listRowAdapter));              setAdapter(mRowsAdapter);        }   MediaDetailsFragment.java
  23. 58.

         @Override        protected  void  onCreate(Bundle  savedInstanceState)

     {                ...                mVideoView  =  (VideoView)  findViewById(R.id.vv_video);                mMediaSession  =  new  MediaSession(this,  getString(R.string.app_name));                mMediaSession.setCallback(new  MediaSessionCallback());                mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS  |                                                              MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);                ...          }          private  void  updateMetadata(final  Movie  movie)  {                final  MediaMetadata.Builder  metadataBuilder  =  new  MediaMetadata.Builder();                metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE,  movie.getMovieTitle());                metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE,  movie.getMovieAuthor());                      Bitmap  image  =  BitmapFactory.decodeResource(getResources(),  movie.getMovieImage());                metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART,  image);                mMediaSession.setMetadata(metadataBuilder.build());        }   PlaybackControlFragment.java Media callbacks to load video when ready and notify user when is not possible to load update current MovieMetaData for “Now Playing” card
  24. 59.

         private  void  addPlaybackControlsRow()  {        

           mPlaybackControlsRow  =  new  PlaybackControlsRow(mSelectedMovie);                mRowsAdapter.add(mPlaybackControlsRow);                ControlButtonPresenterSelector  presenterSelector  =  new  ControlButtonPresenterSelector();                mPrimaryActionsAdapter  =  new  ArrayObjectAdapter(presenterSelector);                mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);                mPlayPauseAction        =  new  PlayPauseAction(getActivity());                mSkipNextAction          =  new  PlaybackControlsRow.SkipNextAction(getActivity());                mSkipPreviousAction  =  new  PlaybackControlsRow.SkipPreviousAction(getActivity());                mFastForwardAction    =  new  PlaybackControlsRow.FastForwardAction(getActivity());                mRewindAction              =  new  PlaybackControlsRow.RewindAction(getActivity());                mPrimaryActionsAdapter.add(mSkipPreviousAction);                mPrimaryActionsAdapter.add(new  PlaybackControlsRow.RewindAction(getActivity()));                mPrimaryActionsAdapter.add(mPlayPauseAction);                mPrimaryActionsAdapter.add(new  PlaybackControlsRow.FastForwardAction(getActivity()));                mPrimaryActionsAdapter.add(mSkipNextAction);        }   PlaybackControlFragment.java
  25. 60.

         private  void  setupRows()  {        

           PlaybackControlsRowPresenter  playbackControlsRowPresenter  =                         new  PlaybackControlsRowPresenter(new  DescriptionPresenter());                playbackControlsRowPresenter.setOnActionClickedListener(new  OnActionClickedListener()  {                        public  void  onActionClicked(Action  action)  {                                if  (action.getId()  ==  mPlayPauseAction.getId())  {                                        if  (mPlayPauseAction.getIndex()  ==  PlayPauseAction.PLAY)  {                                                startProgressAutomation();                                                mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),                                                                mPlaybackControlsRow.getCurrentTime(),  true);                                        }  else  {                                                stopProgressAutomation();                                                mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),                                                                mPlaybackControlsRow.getCurrentTime(),  false);                                        }                                }  else  if  (action.getId()  ==  mSkipNextAction.getId())  {                                        next();                                }                                  ...                ClassPresenterSelector  ps  =  new  ClassPresenterSelector();                ps.addClassPresenter(PlaybackControlsRow.class,  playbackControlsRowPresenter);                ps.addClassPresenter(ListRow.class,  new  ListRowPresenter());                mRowsAdapter  =  new  ArrayObjectAdapter(ps);                addPlaybackControlsRow();                addOtherRows();                setAdapter(mRowsAdapter);        }   PlaybackControlFragment.java
  26. 61.

         private  void  next()  {        

             if  (++mCurrentItem  >=  mItems.size())  {                          mCurrentItem  =  0;                  }   !                if  (mPlayPauseAction.getIndex()  ==  PlayPauseAction.PLAY)  {                          mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),  0,  false);                  }  else  {                          mCallback.onFragmentPlayPause(mItems.get(mCurrentItem),  0,  true);                  }   !                mFfwRwdSpeed  =  INITIAL_SPEED;                  updatePlaybackRow();          }   PlaybackControlFragment.java
  27. 62.

         @Override        public  void  onFragmentPlayPause(Movie  movie,

     int  position,  Boolean  playPause)  {                mVideoView.setVideoPath(movie.getMovieUrl());                if  (position  ==  0  ||  mPlaybackState  ==  LeanbackPlaybackState.IDLE)  {                        setupCallbacks();                        mPlaybackState  =  LeanbackPlaybackState.IDLE;                }                if  (playPause  &&  mPlaybackState  !=  LeanbackPlaybackState.PLAYING)  {                        mPlaybackState  =  LeanbackPlaybackState.PLAYING;                        if  (position  >  0)  {                                mVideoView.seekTo(position);                                mVideoView.start();                        }                }  else  {                        mPlaybackState  =  LeanbackPlaybackState.PAUSED;                        mVideoView.pause();                }                updatePlaybackState(position);                updateMetadata(movie);        }   PlaybackControlActivity.java
  28. 65.

    public  class  UpdateRecommendationsService  extends  IntentService  {   !    

       private  NotificationManager  mNotificationManager;   !        ...                  @Override          protected  void  onHandleIntent(Intent  intent)  {                  if  (mNotificationManager  ==  null)  {                          mNotificationManager  =  (NotificationManager)                                 getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);                  }   !                RecommendationBuilder  builder  =  new  RecommendationBuilder()                                                                                                  .setContext(getApplicationContext())                                                                                                  .setSmallIcon(R.drawable.ic_video);   !                for  (Movie  movie  :  dailyRecommendedVideos())  {                          final  int  id  =  dailyRecommendedVideos.indexOf(movie);                          final  RecommendationBuilder  notificationBuilder  =  builder                                          .setId(id)                                          .setPriority(id)                                          .setTitle(movie.getMovieTitle())                                          .setDescription(movie.getMovieDescription())                                          .setIntent(buildPendingIntent(movie));   !                        Bitmap  image  =  BitmapFactory.decodeResource(getResources(),  movie.getMovieImage());                          notificationBuilder.setBitmap(image);                          Notification  notification  =  notificationBuilder.build();                          mNotificationManager.notify(id,  notification);                          }                  }          }   UpdateRecommendationsService.java wrapper for notification class
  29. 66.

         public  RecommendationBuilder  setBackground(String  uri)  {      

             mBackgroundUri  =  uri;                return  this;        }        public  Notification  build()  {                Log.d(TAG,  "Building  notification  -­‐  "  +  this.toString());                Bundle  extras  =  new  Bundle();                if  (mBackgroundUri  !=  null)  {                        Log.d(TAG,  "Background  -­‐  "  +  mBackgroundUri);                        extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,  mBackgroundUri);                }                Notification  notification  =  new  NotificationCompat.BigPictureStyle(                                new  NotificationCompat.Builder(mContext)                                                .setContentTitle(mTitle)                                                .setContentText(mDescription)                                                .setPriority(mPriority)                                                .setLocalOnly(true)                                                .setOngoing(true)                                                .setColor(mContext.getResources().getColor(R.color.mdevcon_selected_background))                                                .setCategory(Notification.CATEGORY_RECOMMENDATION)                                                .setLargeIcon(mBitmap)                                                .setSmallIcon(mSmallIcon)                                                .setContentIntent(mIntent)                                                .setExtras(extras))                                .build();                return  notification;        }   RecommendationBuilder.java allows to define an image for the car presenter class can use as background
  30. 67.

         private  PendingIntent  buildPendingIntent(Movie  movie)  {      

             Intent  detailsIntent  =  new  Intent(this,  MediaActivity.class);                detailsIntent.putExtra(Movie.class.getSimpleName(),  movie);                TaskStackBuilder  stackBuilder  =  TaskStackBuilder.create(this);                stackBuilder.addParentStack(MediaActivity.class);                stackBuilder.addNextIntent(detailsIntent);                detailsIntent.setAction(movie.hashCode()+"");                PendingIntent  intent  =  stackBuilder.getPendingIntent(0,  PendingIntent.FLAG_UPDATE_CURRENT);                return  intent;        }   UpdateRecommendationsService.java every notification should be unique, otherwise they will be replaced (updated)
  31. 70.

    public  class  SearchFragment  extends  android.support.v17.leanback.app.SearchFragment          

           implements  android.support.v17.leanback.app.SearchFragment.SearchResultProvider  {   !        private  static  final  String  TAG  =  "SearchFragment";          private  static  final  int  SEARCH_DELAY_MS  =  300;   !        private  ArrayObjectAdapter  mRowsAdapter;          private  Handler  mHandler  =  new  Handler();          private  SearchRunnable  mDelayedLoad;   !        @Override          public  void  onCreate(Bundle  savedInstanceState)  {                  super.onCreate(savedInstanceState);   !                mRowsAdapter  =  new  ArrayObjectAdapter(new  ListRowPresenter());                  setSearchResultProvider(this);                  setOnItemClickedListener(getDefaultItemClickedListener());                  mDelayedLoad  =  new  SearchRunnable();          }   SearchFragment.java searching for items should not be done on UI level
  32. 71.

           @Override          public  ObjectAdapter

     getResultsAdapter()  {                  return  mRowsAdapter;          }          @Override          public  boolean  onQueryTextChange(String  newQuery)  {                  queryByWords(newQuery);                  return  true;          }          @Override          public  boolean  onQueryTextSubmit(String  query)  {                  queryByWords(query);                  return  true;          }          private  void  queryByWords(String  words)  {                  mRowsAdapter.clear();                  if  (!TextUtils.isEmpty(words))  {                          mDelayedLoad.setSearchQuery(words);                          mHandler.removeCallbacks(mDelayedLoad);                          mHandler.postDelayed(mDelayedLoad,  SEARCH_DELAY_MS);                  }          }   implements SearchResultProvider user pressed DPAD_CENTER real-time update on query
  33. 73.

    73 versionName  “1.0” • Do not ‘lose’ focus • Provide

    easy shortcuts for common actions • Search within app • Minimum actions required policy • Acquire audio focus when playing content • Make use of visual indicators for more information • Compensate for overscan • Don’t try to reinvent the wheel • Eat your own dog food
  34. 76.

    76 Android Studio http://developer.android.com/sdk/index.html SDK Manager Install Android 5.0.1 (API

    21) AVD Manager Create an Android TV virtual device Support Libraries leanback, recyclerview and cardview buildTools
  35. 78.

    C a r l o s M o t a

    78 @cafonsomota cafonsomota@gmail.com
  36. 80.