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

Android TV Strangelove (Or How I Learned To Sto...

cmota
October 17, 2015

Android TV Strangelove (Or How I Learned To Stop Worrying And Loved The Bomb)

cmota

October 17, 2015
Tweet

More Decks by cmota

Other Decks in Technology

Transcript

  1. Android TV S t r a n g e lo

    v e Or: How I STOP WORRYING AND LOVE THE BOMB LEARNED TO +Carlos Mota @cafonsomota
  2. You’re not going to guess what happened on the latest

    episode of Game of Thrones… - (almost) everyone
  3. 12

  4. Distance to the TV Larger (way larger) screens Always connected

    to the internet No screen rotation (always landscape) Not touchable Use of remote control 16 compileSdkVersion  21
  5. 19 It’s not a tablet on landscape It’s not a

    tablet Accessibility compileSdkVersion  21
  6. 20 It’s not touchable It’s not a tablet on landscape

    It’s not a tablet Accessibility compileSdkVersion  21
  7.        <RelativeLayout            

         android:layout_width="wrap_content"                  android:layout_height="wrap_content"                  android:layout_gravity="center"                  android:focusable="true"                  android:focusableInTouchMode="true">                  ...                  <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 must be focusable (default = true)
  8. ListView LinearLayout ImageView TextView LinearLayout Horizontal Scroll LinearLayout ImageView TextView

    Button Fragment A Fragment B transitions Handle animations Handle D-Pad events 28
  9. 10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:  FATAL  EXCEPTION:  main   10-­‐10  11:30:16.411

     2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:  Process:   com.cmota.gdgportotv,  PID:  2055   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:   java.lang.OutOfMemoryError:  Failed  to  allocate  a  9114908  byte  allocation  with  6013044  free   bytes  and  5MB  until  OOM   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   dalvik.system.VMRuntime.newNonMovableArray(Native  Method)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.graphics.BitmapFactory.nativeDecodeAsset(Native  Method)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:609)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:444)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:988)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.content.res.Resources.loadDrawableForCookie(Resources.java:2474)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.content.res.Resources.loadDrawable(Resources.java:2381)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.content.res.Resources.getDrawable(Resources.java:787)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at   android.content.res.Resources.getDrawable(Resources.java:752)   10-­‐10  11:30:16.411  2055-­‐2055/com.cmota.gdgportotv  E/AndroidRuntime:          at  
  10. Guided Step Fragment Search Fragment Browse Fragment Details Fragment (full

    bleed) PlaybackOverlayFragment Details Fragment
  11. 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 compile  ‘com.android.support.leanback-­‐v17:+’ 33
  12.        <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/gdg_devfest_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
  13. Walkthrough multiple step flows Show previous steps Overlay over content

    Do not disturb the user <class  name=“…GuidedStepFragment”/> 36
  14. @Override   public  GuidanceStylist.Guidance  onCreateGuidance(Bundle  savedInstanceState)  {
      

     String  title  =  getString(R.string.guided_gdg_dev_fest);
        String  breadcrumb  =  getString(R.string.guided_welcome);
        String  description  =  getString(R.string.guided_select_your_gdg_home);
        Drawable  icon  =  getDrawable(R.drawable.gdg_devfest_logo);
 
        return  new  GuidanceStylist.Guidance(title,  description,  breadcrumb,  icon);
 }   @Override
 public  void  onCreateActions(List<GuidedAction>  actions,  Bundle  savedInstanceState)  {
        String[]  countries  =  ItemListWorldWide.WORLDWIDE_GDG;
        for(int  index=0;  index<countries.length;  index++)  {
                addAction(actions,  index,  countries[index]);
        }
 }   InitialGuidedStepsFragment  extends  GuidedStepsFragment
  15. @Override
 public  void  onGuidedActionClicked(GuidedAction  action)  {        

     Intent  intent  =  new  Intent(getActivity(),  MainActivity.class);          intent.putExtra(Values.EXTRA_ARG_COUNTRY,  action.getId());
        startActivity(new  Intent(getActivity(),  MainActivity.class));
        getActivity().finish();
 }   InitialGuidedStepsFragment  extends  GuidedStepsFragment
  16. Multi-pane layout Standard navigational design Smooth transitions Navigate across different

    content Quick access to everything <class  name=“…BrowseFragment”/> 39
  17.        mRowsAdapter  =  new  ArrayObjectAdapter(new  ListRowPresenter());    

         /*  Speakers  */          HeaderItem  headerSpeakers  =  new  HeaderItem(getString(R.string.header_speakers),  null);          ArrayObjectAdapter  speakersAdapter  =  new  ArrayObjectAdapter(new  CardPresenter());          speakersAdapter.add(Data.getInstance().getSpeakersList());          ...          mRowsAdapter.add(new  ListRow(headerEventInfo,      eventsAdapter));          mRowsAdapter.add(new  ListRow(headerSponsorInfo,  sponsorsAdapter));          mRowsAdapter.add(new  ListRow(headerSpeakers,        speakersAdapter));          mRowsAdapter.add(new  ListRow(headerPrevious,        googleIOAdapter));          mRowsAdapter.add(new  ListRow(headerOther,              otherGDGAdapter));          setAdapter(mRowsAdapter); MainBrowserFragment  extends  BrowseFragment custom presenter category name and icon
  18.        @Override          public  ViewHolder

     onCreateViewHolder(final  ViewGroup  parent)  {                  ImageCardView  cardView  =  new  ImageCardView(parent.getContext())  {                          @Override                          public  void  setSelected(boolean  selected)  {                                  int  nBackground  =  mContext.getResources().getColor(R.color.bg_normal);                                  int  sBackground  =  mContext..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);          }   CardPresenter  extends  Presenter required for D-Pad focus alternatively we could create a custom one
  19.        @Override          public  ViewHolder

     onCreateViewHolder(final  ViewGroup  parent)  {                  ...          }          @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  extends  Presenter remove image refs. for GC
  20. MainBrowserFragment  extends  BrowseFragment        @Override      

       public  void  onActivityCreated(Bundle  savedInstanceState)  {                  setOnItemViewSelectedListener(new  ItemViewSelectedListener());          }          private  class  ItemSelectedListener  implements  OnItemViewSelectedListener  {                                    @Override                  public  void  onItemSelected(Presenter.ViewHolder  viewHolder,  Object  item,                                                            RowPresenter.rowViewHolder  rvH,  Row  row)  {                          if  (item  instanceof  Movie)  {                                  mBackgroundRefId  =  ((Movie)  item).getMovieBackgroundImage();                          }  else  {                                  mBackgroundRefId  =  R.drawable.default_background;                                  Log.d(TAG,  "Default  instance  type:  "  +  item);                          }                          startBackgroundTimer(((Movie)  item).getMovieBackgroundImage());                  }          }   change background according to selected movie DPAD_RIGHT, DPAD_LEFT, DPAD_DOWN, DPAD_UP
  21. time between refreshes MainBrowserFragment  extends  BrowseFragment        private

     void  startBackgroundTimer()  {                  mBackgroundTimer  =  new  Timer();                  mBackgroundTimer.schedule(new  UpdateBackgroundTask(),  BACKGROUND_UPDATE_DELAY);          }          private  void  setBackground()  {                  BackgroundManager  backgroundManager  =  BackgroundManager.getInstance(getActivity());                  backgroundManager.attach(getActivity().getWindow());                  backgroundManager.setDrawable(getResources().getDrawable(mBackgroundRefId));          }          private  class  UpdateBackgroundTask  extends  TimerTask  {                  @Override                  public  void  run()  {                          mHandler.post(()  →  {                                          setBackground();                                  });                  }          }  
  22. Detailed information Quick access to related data Full bleed mode

    <class  name=“…DetailsFragment”/> 49
  23.          ClassPresenterSelector  selector  =  new  ClassPresenterSelector();  

             selector.addClassPresenter(DetailsOverviewRow.class,  rowPresenter);            selector.addClassPresenter(ListRow.class,  new  ListRowPresenter());            DetailsOverviewRow  detailsOverviewRow  =  new  DetailsOverviewRow(item);            detailsOverviewRow.setImageDrawable(getResources().getDrawable(item.getMainImage()));            detailsOverviewRow.addAction(new  Action(ACTION_TWITTER,  item.getActionName()));            mRowsAdapter  =  new  ArrayObjectAdapter(selector);            mRowsAdapter.add(detailsOverviewRow);            ArrayObjectAdapter  listRowAdapter  =  new  ArrayObjectAdapter(new  CardPresenter());            for(Item  item  :  itemList)  {                    listRowAdapter.add(item);            }            HeaderItem  header  =  new  HeaderItem(0,  getString(R.string.details_related),  null);            mRowsAdapter.add(new  ListRow(header,  listRowAdapter));            setAdapter(mRowsAdapter); MediaDetailsFragment  extends  DetailsFragment
  24.          DetailsOverviewRowPresenter  rowPresenter  =  new  DetailsOverviewRowPresenter(  

                                                                                                                                         new  DetailsPresenter());            rowPresenter.setOnActionClickedListener(new  OnActionClickedListener()  {                    @Override                    public  void  onActionClicked(Action  action)  {                            if(action.getId()  ==  ACTION_TWITTER)  {                                    Intent  intent  =  new  Intent(getActivity(),  WebActivity.class);                                    intent.putExtra(Values.ARG_EXTRA_ACTION,  item);                                    startActivity(intent);                            }                    }            });           MediaDetailsFragment  extends  DetailsFragment
  25. Video control actions Quick access to related videos Integration with

    third parties <class  name=“…PlaybackControlFragment”/> 54
  26. mMediaSession  =  new  MediaSession(this,  getString(R.string.app_name));   mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS  |    

                                               MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);   setSessionToken(mSession.getSessionToken());   ...   int  result  =   requestAudioFocus(afChangeListener,  AudioManager.STREAM_MUSIC,  AudioManager.AUDIOFOCUS_GAIN);   if(result  ==  AudioManager.AUDIOFOCUS_REQUEST_GRANTED)  {              if(!mMediaSession.isActive())  {                      mMediaSession.setActive(true);            }   }   public  void  registerCallback(MediaSessionCallback  cb)  {              mMediaSession.setCallback(cb);   }     MediaPlaybackService  extends  MediaBrowserService The card will be removed if the application calls setActive(false) or other app requires the audio focus
  27. MediaSessionCallback  extends  MediaSession.Callback        @Override
      

     public  void  onPlay()  {
                playPause(true);                  }
        @Override
        public  void  onPlayFromMediaId(String  mediaId,  Bundle  extras)  {
                setVideoPath(mItemList.getItemById(mediaId).getLinkUrl());
                mPlaybackState  =  LeanbackPlaybackState.PAUSED;
                updateMetadata(mItemList.getItemById(mediaId));
                playPause(extras.getBoolean(Values.AUTO_PLAY));
        }
        @Override
        public  void  onSeekTo(long  pos)  {
                setPosition((int)  pos);
                mVideoView.seekTo(mPosition);
                updatePlaybackState();
        } Pause current video before starting new one UI Update (activity)
  28. MediaSessionCallback  extends  MediaSession.Callback        @Override      

       public  void  onSkipToNext()  {
                PlaybackState.Builder  stateBuilder  =                                                  new  PlaybackState.Builder().setActions(getAvailableActions());
                stateBuilder.setState(PlaybackState.STATE_SKIPPING_TO_NEXT,  0,  1.0f);
                mMediaSession.setPlaybackState(stateBuilder.build());                  if  (++mCurrentItem  >=  mRelatedVideosList.size())  {                        mCurrentItem  =  0;
                }                  Bundle  bundle  =  new  Bundle();
                bundle.putBoolean(PlaybackOverlayActivity.AUTO_PLAY,  true);
                String  nextId  =  mRelatedVideos.get(mCurrentItem).getId());
                getMediaController().getTransportControls().playFromMediaId(nextId,  bundle);
        } Set media session to skip current video position in time and playback speed
  29. MediaSessionCallback  extends  MediaSession.Callback private  void  updateMetadata(Item  item)  {
    

       MediaMetadata.Builder  metadataBuilder  =  new  MediaMetadata.Builder();
 
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID,                                                              item.getId());
      metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE,                                                              item.getTitle());
              metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE,                                                              item.getSubtitle());
              metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION,                                                            item.getDescription());
              metadataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION,  mDuration);
 }  
  30. PlaybackControlFragment  extends  PlaybackOverlayFragment ControlButtonPresenterSelector  presenterSelector  =  new  ControlButtonPresenterSelector();
 mPrimaryActionsAdapter  =

     new  ArrayObjectAdapter(presenterSelector);
 mSecondaryActionsAdapter  =  new  ArrayObjectAdapter(presenterSelector);
 mPlaybackControlsRow  =  new  PlaybackControlsRow(mSelectedItem);   mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
 mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter);
 
 mPlayPauseAction  =  new  PlayPauseAction(getActivity());   mSkipPreviousAction  =  new  SkipPreviousAction(getActivity());   mPrimaryActionsAdapter.add(mPlayPauseAction);   mPrimaryActionsAdapter.add(mSkipPreviousAction);  
  31. PlaybackControlFragment  extends  PlaybackOverlayFragment PlaybackControlsRowPresenter  playbackControlsRowPresenter  =  new      

                                                                             PlaybackControlsRowPresenter(new  DescriptionPresenter());
 playbackControlsRowPresenter.setOnActionClickedListener((action)  →  {                  if  (action.getId()  ==  mPlayPauseAction.getId())  {
                        togglePlayback(mPlayPauseAction.getIndex()  ==  PlayPauseAction.PLAY);
                }   ...   ClassPresenterSelector  presenterSelector  =  new  ClassPresenterSelector();
 presenterSelector.addClassPresenter(PlaybackControlsRow.class,  playbackControlsRowPresenter);
 presenterSelector.addClassPresenter(ListRow.class,  new  ListRowPresenter());   mRowsAdapter  =  new  ArrayObjectAdapter(presenterSelector);
 mRowsAdapter.add(mPlaybackControlsRow);   setAdapter(mRowsAdapter);   

  32. MediaControllerCallback  extends  MediaController.Callback        @Override
      

     public  void  onPlaybackStateChanged(PlaybackState  state)  {
                if  (state.getState()  ==  STATE_PLAYING  &&  mCurrentPlaybackState  !=  STATE_PLAYING)  {
                        mCurrentPlaybackState  =  STATE_PLAYING;
                        startProgressAutomation();
                        setFadingEnabled(true);
                        mPlayPauseAction.setIndex(PAUSE);
                        mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PAUSE));
                        notifyChanged(mPlayPauseAction);
            ...
        }
 
        @Override
        public  void  onMetadataChanged(MediaMetadata  metadata)  {
                updateItemView(metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE),
                                              metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE),
                                              metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI),
                                              metadata.getString(MediaMetadata.METADATA_KEY_DURATION)
                );
        } UI Update (fragment)
  33. RecommendationBuilder.java        Notification  notification  =  new  NotificationCompat.BigPictureStyle(  

                             new  NotificationCompat.Builder(mContext)                                            .setContentTitle(item.getTitle())                                            .setContentText(item.getDescription())                                            .setPriority(NotificationCompat.PRIORITY_MAX)                                            .setLocalOnly(true)                                            .setOngoing(true)                                            .setColor(getResources().getColor(R.color.selected_background))                                            .setCategory(Notification.CATEGORY_RECOMMENDATION)                                            .setLargeIcon(item.getFullImage())                                            .setSmallIcon(item.getIcon())                                            .setContentIntent(item.getIntent())                                            .setExtras(NOTIFICATION.EXTRA_BACKGROUND_IMAGE_URI,  item.getBgImage())                            .build(); defines an image to be displayed on the background when the recommendation is selected
  34. public  class  SearchFragment  extends  SearchFragment  implements  SearchResultProvider  {    

         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();          }   InAppSearchFragment  extends  SearchFragment searching for items should not be done on UI level
  35.        @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)  {                  mDelayedLoad.setSearchQuery(words);                  mHandler.removeCallbacks(mDelayedLoad);                  mHandler.postDelayed(mDelayedLoad,  SEARCH_DELAY_MS);          }          ...                  HeaderItem  header  =  new  HeaderItem(0,  getString(R.string.search_results));          mRowsAdapter.add(new  ListRow(header,  adapter));   user pressed DPAD_CENTER real-time query update InAppSearchFragment  ...  implements  SearchResultProvider update UI with new elements
  36. 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 versionName  “1.0” 72