How to enable developer mode in Android TV

Android TV is of course Android, so we can enable developer mode in the same way with Android phones & tablets.

Go to “Settings” at the bottom of LeanbackLauncher

dev1

Select “About” icon at the top row, most right side

Press “Build” button, located at the bottom for 7 times.

Go back “Home” and Go to “Settings” again, Developer options are now available on the System Preferences row, right most side.

That’s all!

Appendix

By the way, each Android version have hidden playful app, you can check it on your Android TV as well.

Go to Settings → About → Version and press Version for many times, you can enjoy game 🙂

If Android version is 5 (Lollipop), this kind of display will appear after pressing “version” several times.

This game is too difficult if you play it in Android TV with remote controller… Please try it!

Install Android phone’s application to Android TV

esFileExplorer1

[2016.3.13 added] This post explains sideloading apps via USB. It is also possible to transfer file via Wifi. See Install Android phone’s app on Android TV via Wifi.

Extract & install apk file using ES file explorer

If you have Android phone, you can extract .apk file (Android application file) and try to move it to your Android TV. I recommend to use “ES file explorer” to extract & install apk file.

  1. Get apps that you want to install in your Android phone from Google play.
  2. Get .apk file from Your Android phone using ES file explorer app on Android phone.
    <Reference>
    – Using ES File Explorer To Make APK Backups of Android Apps and Games
    – Top 5 Ways to Extract apk From Android (NO ROOT)
    – How To Extract .apk File From Android Phone
  3. Move extracted .apk file to USB drive.
  4. plug-in USB drive to Android TV, install .apk file using ES file explorer app on Android TV side.

I will explain this part in detail.

4-1. Install ES File Explorer Manager app on Android TV.

esFileExplorer1

4-2. Open ES file explorer, with plugging in USB drive to Android TV.
Go to left side bar and you can find USB in Local tab.

device

4-3. Open it.

device

4-4. You can find apk file inside USB drive now.

esFileExplorer5

4-5. Just select apk file to install it!

esFileExplorer6

* Note that to install apk file, we need to set some security settings.
Go to “Settings” → “Security & Restrictions” in Personal Row → “Unknown sources” → Change to “On” to allow install apk file externally.

4-6. Done! If Installled application is not appeared on LeanbackLauncher, try to find in Settings → Apps.

In Another way, if you are Android app developer, you may use adb command to install apk.

3′. Move extracted .apk file to your PC.
4′. install apk file using adb command from PC.
$ adb install -r [your_extracted_apk_filename]

So basically, you can try most of the Android phone’s apps in your Android TV too!

* CAUTION: The tips written here is not officially supported. I don’t take any responsibility caused by this Technique. Try it with your own responsibility.

MediaSession & MediaController
– Android TV app hands on Tutorial 9

NowPlayingCard

Video Controls implementation with MediaSession

The sample implementation is done in Google’s latest sample Android TV application. AOSP sample application implementation does not have MediaSession implementation yet (in API 21, 22).

In previous chapter, I explained that following Video controls are needed to be impmented.

  1. Action’s UI update part (done in previous chapter)
  2. Video control part (done in previous chapter) MediaSession implementation, Video control via MediaController’s TransportControls (this chapter)
  3. – MediaSession can handle the action when user presses TV remote controller’s video control button.
    – It allows other activity to inherite video control. Especially LeanbackLauncher, Home display, can play video in background.  
  4. set MediaMetadata to MediaSession (this chapter)
    – “Now playing card” will appear at the top of recommendation row.

In this chapter, we proceed to implement Video controls using MediaSession. We can pass VideoView control to LeanbackLauncher by using MediaSession, which results to achieve playing video background in LeanbackLauncher. 

For 3, we create MediaSession in PlaybackOverlayActivity, and control it from MediaController in PlaybackOverlayFragment.

For 4, MediaSession’s Metadata is updated using MediaMetadata & PlaybackState class to update “Now playing card”.

I recommend you to read Displaying a Now Playing Card, for official explanation.

This chapter’s implementation is almost independent with previous chapter’s implementation. Before implementing MediaSession, I will implement requestVisibleBehind method, so that we can play video in background of LeanbackLauncher app.

Implement requestVisibleBehind

This method is added in API 21 (Lolipop), AOSP explanation explains that

If this call is successful then the activity will remain visible after onPause() is called, and is allowed to continue playing media in the background.

Sample implementation is as follows.

    @Override
    public void onPause() {
        super.onPause();
        if (!requestVisibleBehind(true)) {
            // Try to play behind launcher, but if it fails, stop playback.
            mPlaybackController.playPause(false);
        }
    }

After this implementation, when you play video contents in your application and press “Home” key to go back LeanbackLauncher, the video is remain playing in background. .

Class structure of this chapter

We handle 3 classes in this chapter.

  • PlaybackOverlayActivity    
     – manage lifecycle, pass intent information to PlaybackController
     – MediaSession’s life time is linked with activity
  • PlaybackOverlayFragment
     – handle UI of PlaybackControlsRow
     – MediaController Callback function is used to update UI according to the current playback state
  • PlaybackController              
     – manage video playback
     – video control functions
     – MediaSessionCallback for receiving Video control key from TV remote controller

Create & release MediaSession

So far, we cannot control this video by using video control key in remote controller. Let’s implement MediaSession to define the action for each video control key in remote controller. At first, we create MediaSession in Constructor of PlaybackController, which is invoked by PlaybackOverlayActivity.

    public PlaybackController(Activity activity) {
        mActivity = activity;
        // mVideoView = (VideoView) activity.findViewById(VIDEO_VIEW_RESOURCE_ID);
        createMediaSession(mActivity);
    }

    private void createMediaSession(Activity activity) {
        if (mSession == null) {
            mSession = new MediaSession(activity, MEDIA_SESSION_TAG);
            mMediaSessionCallback = new MediaSessionCallback();
            mSession.setCallback(mMediaSessionCallback);
            mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
                    MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);

            mSession.setActive(true);
            activity.setMediaController(new MediaController(activity, mSession.getSessionToken()));
        }
    }

We can set callback, MediaSessionCallback, to MediaSession. It defines the precise behavior of each video control button which will be explained later.  

setFlags method with the argument FLAG_HANDLES_MEDIA_BUTTONS & FLAG_HANDLES_TRANSPORT_CONTROLS are necessary to enable remote controller key to control video.

After create, we must release MediaSession when it finishes.

    public void releaseMediaSession() {
        if(mSession != null) {
            mSession.release();
        }
    }

Video control functions

Video control functions are implemented in MediaSessionCallback class. As name suggests, each video control action is implemented in corresponding callback function. This callback is called from 2 ways, which is “remote controller media key” or “UI video control button in PlaybackControlsRow”.

    public void playPause(boolean doPlay) {

        if (mCurrentPlaybackState == PlaybackState.STATE_NONE) {
            /* Callbacks for mVideoView */
            setupCallbacks();
        }

        //if (doPlay && mCurrentPlaybackState != PlaybackState.STATE_PLAYING) {
        if (doPlay) { // Play
            Log.d(TAG, "playPause: play");
            if(mCurrentPlaybackState == PlaybackState.STATE_PLAYING) {
                /* if current state is already playing, do nothing */
                return;
            } else {
                mCurrentPlaybackState = PlaybackState.STATE_PLAYING;
                mVideoView.start();
                mStartTimeMillis = System.currentTimeMillis();
            }
        } else { // Pause
            Log.d(TAG, "playPause: pause");
            if(mCurrentPlaybackState == PlaybackState.STATE_PAUSED) {
                /* if current state is already paused, do nothing */
                return;
            } else {
                mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
            }
            setPosition(mVideoView.getCurrentPosition());
            mVideoView.pause();

        }

        updatePlaybackState();
    }

    public void fastForward() {
        if (mDuration != -1) {
            // Fast forward 10 seconds.
            setPosition(getCurrentPosition() + (10 * 1000));
            mVideoView.seekTo(mPosition);
        }

    }

    public void rewind() {
        // rewind 10 seconds
        setPosition(getCurrentPosition() - (10 * 1000));
        mVideoView.seekTo(mPosition);
    }


    private class MediaSessionCallback extends MediaSession.Callback {
        @Override
        public void onPlay() {
            playPause(true);
        }

        @Override
        public void onPause() {
            playPause(false);
        }

        @Override
        public void onSkipToNext() {
            if (++mCurrentItem >= mItems.size()) { // Current Item is set to next here
                mCurrentItem = 0;
            }

            Movie movie = mItems.get(mCurrentItem);
            //Movie movie = VideoProvider.getMovieById(mediaId);
            if (movie != null) {
                setVideoPath(movie.getVideoUrl());
                //mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
                //updateMetadata(movie);
                updateMetadata();
                playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
            } else {
                Log.e(TAG, "onSkipToNext movie is null!");
            }

        }


        @Override
        public void onSkipToPrevious() {
            if (--mCurrentItem < 0) { // Current Item is set to previous here
                mCurrentItem = mItems.size()-1;
            }

            Movie movie = mItems.get(mCurrentItem);
            //Movie movie = VideoProvider.getMovieById(mediaId);
            if (movie != null) {
                setVideoPath(movie.getVideoUrl());
                //mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
                updateMetadata();
                playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
            } else {
                Log.e(TAG, "onSkipToPrevious movie is null!");
            }
        }

        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            mCurrentItem = Integer.parseInt(mediaId);
            Movie movie = mItems.get(mCurrentItem);
            //Movie movie = VideoProvider.getMovieById(mediaId);
            if (movie != null) {
                setVideoPath(movie.getVideoUrl());
                // mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
                // updateMetadata(movie);
                updateMetadata();
                playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
            }
        }

        @Override
        public void onSeekTo(long pos) {
            setPosition((int) pos);
            mVideoView.seekTo(mPosition);
            updatePlaybackState();
        }

        @Override
        public void onFastForward() {
            fastForward();
        }

        @Override
        public void onRewind() {
            rewind();
        }
    }

Video control by remote controller key

To enable video control from remote controller, we need to explicitly set available actions to MediaSession. Available actions are defined in PlaybackState class, sample implementation is below.

    private void updatePlaybackState() {
        PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
                .setActions(getAvailableActions());
        int state = PlaybackState.STATE_PLAYING;
        if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) {
            state = PlaybackState.STATE_PAUSED;
        }
        stateBuilder.setState(state, getCurrentPosition(), 1.0f);
        mSession.setPlaybackState(stateBuilder.build());
    }

    private long getAvailableActions() {
        long actions = PlaybackState.ACTION_PLAY |
                PlaybackState.ACTION_PAUSE |
                PlaybackState.ACTION_PLAY_PAUSE |
                PlaybackState.ACTION_REWIND |
                PlaybackState.ACTION_FAST_FORWARD |
                PlaybackState.ACTION_SKIP_TO_PREVIOUS |
                PlaybackState.ACTION_SKIP_TO_NEXT |
                PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
                PlaybackState.ACTION_PLAY_FROM_SEARCH;
        return actions;
    }

In this example, available actions are decided in getAvailableActions method, by adding actions using logical disjunction.

Video control from UI – MediaController.getTransportControls

To control MediaSession from PlaybackControlsRow in VideoDetailsFragment, we use MediaController. The MediaController is created in the constructor of PlaybackController and it owns token of MediaSession.

When user clicks video control button, it will invoke MediaSessionCallback method using MediaController.getTransportControls() method.

        /* onClick */
        playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() {
            public void onActionClicked(Action action) {
                if (action.getId() == mPlayPauseAction.getId()) {
                    /* PlayPause action */
                    if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY) {
                        mMediaController.getTransportControls().play();
                    } else if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PAUSE) {
                        mMediaController.getTransportControls().pause();
                    }
                } else if (action.getId() == mSkipNextAction.getId()) {
                    /* SkipNext action */
                    mMediaController.getTransportControls().skipToNext();
                } else if (action.getId() == mSkipPreviousAction.getId()) {
                    /* SkipPrevious action */
                    mMediaController.getTransportControls().skipToPrevious();
                } else if (action.getId() == mFastForwardAction.getId()) {
                    /* FastForward action  */
                    mMediaController.getTransportControls().fastForward();
                } else if (action.getId() == mRewindAction.getId()) {
                    /* Rewind action */
                    mMediaController.getTransportControls().rewind();
                }
        }

Video control part is done. However, we need to update UI of PlayControlsRow depending according to the video control.

Updating UI of VideoDetailsFragment 

We need to change the UI when video control action has executed, and video playback status has changed. We can get this event by using MediaController’s callback function. 2 callback methods are introduced in the following. 

  • onPlaybackStateChanged
  • onMetadataChanged

To use these callback methods, you can make subclass which extends MediaController.Callback class, and override these methods. To use this class, we can call MediaController’s registerCallback/unregisterCallback method to get event of MediaController.

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mMediaController = getActivity().getMediaController();
        Log.d(TAG, "register callback of mediaController");
        if(mMediaController == null){
            Log.e(TAG, "mMediaController is null");
        }
        mMediaController.registerCallback(mMediaControllerCallback);

    }

    @Override
    public void onDetach() {
        if (mMediaController != null) {
            Log.d(TAG, "unregister callback of mediaController");
            mMediaController.unregisterCallback(mMediaControllerCallback);
        }
        super.onDetach();
    }

    private class MediaControllerCallback extends MediaController.Callback {
        @Override
        public void onPlaybackStateChanged(final PlaybackState state) {
            Log.d(TAG, "playback state changed: " + state.toString());
        }

        @Override
        public void onMetadataChanged(final MediaMetadata metadata) {
            Log.d(TAG, "received update of media metadata");
        }
    }

Updating video control icons in onPlaybackStateChanged 

Update PlaybackState

PlaybackState is updated in PlaybackController.

    private void updatePlaybackState() {
        PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
                .setActions(getAvailableActions());
        int state = PlaybackState.STATE_PLAYING;
        if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) {
            state = PlaybackState.STATE_PAUSED;
        }
        // stateBuilder.setState(state, mPosition, 1.0f);
        stateBuilder.setState(state, getCurrentPosition(), 1.0f);
        mSession.setPlaybackState(stateBuilder.build());
    }

For example, it will be called in playPause method. When user start play video state will change from STATE_PLAYING to STATE_PAUSED, or vice versa. PlaybackState update is set (notified) to MediaSession.

Callback

When PlaybackState has changed by setPlaybackState above, this event can be received with onPlaybackStateChanged callback. We can update play/pause icon in PlaybackControlsRow.

    private class MediaControllerCallback extends MediaController.Callback {
        @Override
        public void onPlaybackStateChanged(final PlaybackState state) {
            Log.d(TAG, "playback state changed: " + state.toString());
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (state.getState() == PlaybackState.STATE_PLAYING) {
                        mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PLAYING);
                        startProgressAutomation();
                        // setFadingEnabled(false);
                        mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PAUSE);
                        mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PAUSE));
                        notifyChanged(mPlayPauseAction);
                    } else if (state.getState() == PlaybackState.STATE_PAUSED) {
                        mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PAUSED);
                        // setFadingEnabled(false);
                        mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PLAY);
                        mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PLAY));
                        notifyChanged(mPlayPauseAction);
                    }

                    int currentTime = (int) state.getPosition();
                    mPlaybackControlsRow.setCurrentTime(currentTime);
                    // mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
                    mPlaybackControlsRow.setBufferedProgress(mPlaybackController.calcBufferedTime(currentTime));

                }
            });
        }

        ...
    }

update Media information in onMetadataChanged

Update MediaMetadata

MediaMetadata class is used to set metadata information of the video. We can set each attribute of metadata by using put method in MediaMetadata.Builder. Again, MediaMetadata update is set (notified) to MediaSession.

    public void updateMetadata(Movie movie) {
        final MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();

        String title = movie.getTitle().replace("_", " -");

        metadataBuilder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(movie.getId()));
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title);
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, movie.getStudio());
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION, movie.getDescription());
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, movie.getCardImageUrl());
        metadataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, mDuration);

        // And at minimum the title and artist for legacy support
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
        metadataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, movie.getStudio());

        Glide.with(mActivity)
                .load(Uri.parse(movie.getCardImageUrl()))
                .asBitmap()
                .into(new SimpleTarget<Bitmap>(500, 500) {
                    @Override
                    public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {

                        metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap);
                        mSession.setMetadata(metadataBuilder.build());
                    }
                });
    }

By setting correct MediaMetadata to MediaSession, Android TV will show Now Playing Card on LeanbackLauncher, which will be explained again later.

Callback

When MediaMetadata has changed by setMetadata above, this event can be received with onMetadataChanged callback. We can update item value of PlaybackControlsRow.

    private class MediaControllerCallback extends MediaController.Callback {

        ...

        @Override
        public void onMetadataChanged(final MediaMetadata metadata) {
            Log.d(TAG, "received update of media metadata");
                    updateMovieView(
                            metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE),
                            metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE),
                            metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI),
                            metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)
                    );
        }
    }

    private void updateMovieView(String title, String studio, String cardImageUrl, long duration) {
        Log.d(TAG, "updateMovieView");

        if (mPlaybackControlsRow.getItem() != null) {
            Movie item = (Movie) mPlaybackControlsRow.getItem();
            item.setTitle(title);
            item.setStudio(studio);
        } else {
            Log.e(TAG, "mPlaybackControlsRow.getItem is null!");
        }
        mPlaybackControlsRow.setTotalTime((int) duration);
        mPlaybackControlsRow.setCurrentTime(0);
        mPlaybackControlsRow.setBufferedProgress(0);
        mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());

        // Show the video card image if there is enough room in the UI for it.
        // If you have many primary actions, you may not have enough room.
        if (SHOW_IMAGE) {
            mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow);
            updateVideoImage(cardImageUrl);
        }
    }


Build and run

Now video playcontrol is possible from both clicking button on PlayControlsRow and pushing media key in remote controller. In the above video, I’m controlling video contents via remote controller and not using UI on the screen 🙂

Now Playing Card

NowPlayingCard
Now Playing Card on Leanback Launcher screen.

If the MediaMetadata is correctly set to MediaSession, Now Playing Card will appear to the LeanbackLauncher (Home display). It notifies information of current playing media to user. Also, Now Playing Card enables user to go back to your app to control video (pause/go to next video etc). 

Reference

Source code is on github.

Video Controls minimum implementation
– Android TV app hands on Tutorial 8

action_icon_change

Video Controls minimum implementation

Video is streamed in VideoView.

* I referred Google’s latest sample Android TV application. AOSP sample application implementation is defferent.

For Video controls, we have several stuff to explain.

  1. Action’s UI update part (this chapter)
  2. Video control part (this chapter)
  3. MediaSession implementation, Video control via MediaController’s TransportControls (next chapter)
    – MediaSession can handle the action when user presses TV remote controller’s video control button.
    – It allows other activity to inherite video control. Especially LeanbackLauncher, Home display, can play video in background.  
  4. set MediaMetadata to MediaSession (next chapter)
    – “Now playing card” will appear at the top of recommendation row.

In this chapter, Video controls implementation is explained. Since Google’s sample application implements all 1~4, source code is bit long and difficult to understand for beginners. I did a minimum implementation for only 1~2. I will just explain these implementation for each part in this chapter, so please download & refer source code on github at first (I did a refactoring for using MovieProvider class to prepare movie contents). The Technic in this chapter is general in Android.

Next chapter I will explain about MediaSession implementation. We can pass VideoView control to LeanbackLauncher by using MediaSession, which results to achieve playing video background in LeanbackLauncher. 

VideoView handling 

PlaybackOverlayActivity need to have VideoView field variable “mVideoView” to control video.

public class PlaybackOverlayActivity extends Activity {

    private static final String TAG = PlaybackOverlayActivity.class.getSimpleName();

    private VideoView mVideoView;

    private LeanbackPlaybackState mPlaybackState = LeanbackPlaybackState.IDLE;

    private int mPosition = 0;
    private long mStartTimeMillis;
    private long mDuration = -1;

    /*
     * List of various states that we can be in
     */
    public enum LeanbackPlaybackState {
        PLAYING, PAUSED, IDLE
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_playback_overlay);

        loadViews();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopPlayback();
        mVideoView.suspend();
        mVideoView.setVideoURI(null);
    }

    private void loadViews() {
        mVideoView = (VideoView) findViewById(R.id.videoView);
        mVideoView.setFocusable(false);
        mVideoView.setFocusableInTouchMode(false);

        Movie movie = (Movie) getIntent().getSerializableExtra(DetailsActivity.MOVIE);
        setVideoPath(movie.getVideoUrl());

    }

    public void setVideoPath(String videoUrl) {
        setPosition(0);
        mVideoView.setVideoPath(videoUrl);
        mStartTimeMillis = 0;
        mDuration = Utils.getDuration(videoUrl);
    }

    private void stopPlayback() {
        if (mVideoView != null) {
            mVideoView.stopPlayback();
        }
    }

Implement setOnActionClickedListener & onActionClicked callback 

To assign action of each video control button, we use setOnActionClickedListener which is a method of PlaybackControlsRowPresenter.

    private void setUpRows() {

        ...
 
        /* add ListRow to second row of mRowsAdapter */
        addOtherRows();

        /* onClick */
        playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() {
            public void onActionClicked(Action action) {
                if (action.getId() == mPlayPauseAction.getId()) {
                    /* PlayPause action */
                    togglePlayback(mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY);
                } else if (action.getId() == mSkipNextAction.getId()) {
                    /* SkipNext action */
                    next(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
                } else if (action.getId() == mSkipPreviousAction.getId()) {
                    /* SkipPrevious action */
                    prev(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
                } else if (action.getId() == mFastForwardAction.getId()) {
                    /* FastForward action  */
                    fastForward();
                } else if (action.getId() == mRewindAction.getId()) {
                    /* Rewind action */
                    rewind();
                }
                if (action instanceof PlaybackControlsRow.MultiAction) {
                    /* Following action is subclass of MultiAction
                     * - PlayPauseAction
                     * - FastForwardAction
                     * - RewindAction
                     * - ThumbsAction
                     * - RepeatAction
                     * - ShuffleAction
                     * - HighQualityAction
                     * - ClosedCaptioningAction
                     */
                    notifyChanged(action);
                }
            }
        });

        setAdapter(mRowsAdapter);

    }

From here, Each action’s implementation is explained. Note that it is important to differentiate “UI update part” and “Video control part”, because Video control part will move to MediaSession in next chapter.

In the source code, I implemented “UI update part” in PlaybackOverlayFragment.java, while “Video control part” is implemented in PlaybackOverlayActivity.java.

PlayPauseAction

    private void togglePlayback(boolean playPause) {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).playPause(playPause);

        /* UI control part */
        playbackStateChanged();
    }

Video control part will handle play/pause video in VideoView.

public void playPause(boolean doPlay) {
    if (mPlaybackState == LeanbackPlaybackState.IDLE) {
        /* Callbacks for mVideoView */
        setupCallbacks();
    }

    if (doPlay && mPlaybackState != LeanbackPlaybackState.PLAYING) {
        mPlaybackState = LeanbackPlaybackState.PLAYING;
        if (mPosition > 0) {
            mVideoView.seekTo(mPosition);
        }
        mVideoView.start();
        mStartTimeMillis = System.currentTimeMillis();
    } else {
        mPlaybackState = LeanbackPlaybackState.PAUSED;
        int timeElapsedSinceStart = (int) (System.currentTimeMillis() - mStartTimeMillis);
        setPosition(mPosition + timeElapsedSinceStart);
        mVideoView.pause();
    }
}

private void setupCallbacks() {

    mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {

        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            mVideoView.stopPlayback();
            mPlaybackState = LeanbackPlaybackState.IDLE;
            return false;
        }
    });

    mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mp) {
            if (mPlaybackState == LeanbackPlaybackState.PLAYING) {
                mVideoView.start();
            }
        }
    });

    mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {
            mPlaybackState = LeanbackPlaybackState.IDLE;
        }
    });
}

UI control part will handle 

  • Toggling icon of Play/Pause
  • Update current time of video
    public void playbackStateChanged() {

        if (mCurrentPlaybackState != PlaybackState.STATE_PLAYING) {
            mCurrentPlaybackState = PlaybackState.STATE_PLAYING;
            startProgressAutomation();
            setFadingEnabled(true);
            mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PAUSE);
            mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PAUSE));
            notifyChanged(mPlayPauseAction);
        } else if (mCurrentPlaybackState != PlaybackState.STATE_PAUSED) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
            stopProgressAutomation();
            //setFadingEnabled(false); // if set to false, PlaybackcontrolsRow will always be on the screen
            mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PLAY);
            mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PLAY));
            notifyChanged(mPlayPauseAction);
        }

        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);

    }

    private void startProgressAutomation() {
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    int updatePeriod = getUpdatePeriod();
                    int currentTime = mPlaybackControlsRow.getCurrentTime() + updatePeriod;
                    int totalTime = mPlaybackControlsRow.getTotalTime();
                    mPlaybackControlsRow.setCurrentTime(currentTime);
                    mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);

                    if (totalTime > 0 && totalTime <= currentTime) {
                        stopProgressAutomation();
                        //next(true);
                    } else {
                        mHandler.postDelayed(this, updatePeriod);
                    }
                }
            };
            mHandler.postDelayed(mRunnable, getUpdatePeriod());
        }
    }

    private void stopProgressAutomation() {
        if (mHandler != null && mRunnable != null) {
            mHandler.removeCallbacks(mRunnable);
            mRunnable = null;
        }
    }

Rewind & FastForward

    private void fastForward() {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).fastForward();

        /* UI part */
        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
    }

    private void rewind() {
        /* Video control part */
        ((PlaybackOverlayActivity) getActivity()).rewind();

        /* UI part */
        int currentTime = ((PlaybackOverlayActivity) getActivity()).getPosition();
        mPlaybackControlsRow.setCurrentTime(currentTime);
        mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
    }

Here, rewind & fast forward Video control implementations are done in easy way, just rewind/fast forward 10 seconds from current position.

    public void fastForward() {
        if (mDuration != -1) {
            // Fast forward 10 seconds.
            setPosition(mVideoView.getCurrentPosition() + (10 * 1000));
            mVideoView.seekTo(mPosition);
        }
    }

    public void rewind() {
        // rewind 10 seconds
        setPosition(mVideoView.getCurrentPosition() - (10 * 1000));
        mVideoView.seekTo(mPosition);
    }

UI control part is updating current time of video.

SkipPrevious & SkipNext

    private void next(boolean autoPlay) {
        /* Video control part */
        if (++mCurrentItem >= mItems.size()) { // Current Item is set to next here
            mCurrentItem = 0;
        }

        if (autoPlay) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
        }

        Movie movie = mItems.get(mCurrentItem);
        if (movie != null) {
            ((PlaybackOverlayActivity) getActivity()).setVideoPath(movie.getVideoUrl());
            ((PlaybackOverlayActivity) getActivity()).setPlaybackState(PlaybackOverlayActivity.LeanbackPlaybackState.PAUSED);
            ((PlaybackOverlayActivity) getActivity()).playPause(autoPlay); 
        }

        /* UI part */
        playbackStateChanged();
        updatePlaybackRow(mCurrentItem);
    }

    private void prev(boolean autoPlay) {
        /* Video control part */
        if (--mCurrentItem < 0) { // Current Item is set to previous here
            mCurrentItem = mItems.size() - 1;
        }
        if (autoPlay) {
            mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
        }

        Movie movie = mItems.get(mCurrentItem);
        if (movie != null) {
            ((PlaybackOverlayActivity) getActivity()).setVideoPath(movie.getVideoUrl());
            ((PlaybackOverlayActivity) getActivity()).setPlaybackState(PlaybackOverlayActivity.LeanbackPlaybackState.PAUSED);
            ((PlaybackOverlayActivity) getActivity()).playPause(autoPlay);
        }

        /* UI part */
        playbackStateChanged();
        updatePlaybackRow(mCurrentItem);
    }

For Video control part, 2 functions are doing same thing except for first line. mCurrentItem is set to previous/next followed by setting proper video path by using setVideoPath method & play/pause depending on current play/pause status by using playPause method.

UI control part, first line calls playbackStateChanged() method, but it is only necessary to control startProgressAutomation/stopProgressAutomation to update current time status of video. updateplaybackRow method is to update DetailsDescription information of video content.

    private void updatePlaybackRow(int index) {
        Log.d(TAG, "updatePlaybackRow");
        if (mPlaybackControlsRow.getItem() != null) {
            Movie item = (Movie) mPlaybackControlsRow.getItem();
            item.setTitle(mItems.get(mCurrentItem).getTitle());
            item.setStudio(mItems.get(mCurrentItem).getStudio());

            mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
            /* total time is necessary to show video playing time progress bar */
            int duration = (int) Utils.getDuration(mItems.get(mCurrentItem).getVideoUrl());
            Log.i(TAG, "videoUrl: " + mItems.get(mCurrentItem).getVideoUrl());
            Log.i(TAG, "duration = " + duration);
            mPlaybackControlsRow.setTotalTime(duration);
            mPlaybackControlsRow.setCurrentTime(0);
            mPlaybackControlsRow.setBufferedProgress(0);
        }
        if (SHOW_IMAGE) {
            mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow);
            updateVideoImage(mItems.get(mCurrentItem).getCardImageURI());
        }
    }


ThumbUp & ThumbDown & Repeat & Shuffle & HighQuality & ClosedCaptioning & MoreActions

How to toggle icon’s color? You can change by setting index of the action. Implement below in onActionClicked method to check how each index setting will behave.

                    /* Change icon */
                    if (action instanceof PlaybackControlsRow.ThumbsUpAction ||
                            action instanceof PlaybackControlsRow.ThumbsDownAction ||
                            action instanceof PlaybackControlsRow.RepeatAction ||
                            action instanceof PlaybackControlsRow.ShuffleAction ||
                            action instanceof PlaybackControlsRow.HighQualityAction ||
                            action instanceof PlaybackControlsRow.ClosedCaptioningAction) {
                        ((PlaybackControlsRow.MultiAction) action).nextIndex();
                    }
action_icon_change
Action icon change in SecondaryActionsAdapter.


Build & Run

Now you can check that Video control is working correctly for PrimaryRow.
*SecondaryRow’s action implementation may differ depending on your desire and I will skip here.

* Note. I could take VideoView’s image by screen recording, but I couldn’t take VideoView’s screen capture via Android studio’s debugging tool… (I’m using Sony Android TV for development of Android TV now.)

Again source code is on github.

Next chapter, we implement MediaSession.

PlaybackOverlayActivity & PlaybackOverlayFragment
– Android TV application hands on tutorial 7

PlaybackOverview1-2015-07-14-200711

PlaybackOverlayActivity & PlaybackOverlayFragment – Theory

I will explain about only UI part in this chapter, video control is explained in next chapter.

We will implement UI for handling video contents.

PlaybackOverlayActivity & PlaybackOverlayFragment – Implementation

Creating PlaybackOverlayActivity & PlaybackOverlayFragment is the same way as in introduced previously.

PlaybackOverlayActivity

New → Activity → BlankActivity

Activity Name: PlaybackOverlayActivity
Layout Name: activity_playback_overlay
 …

This PlaybackOverlayActivity will refer activity_playback_overlay.xml file in res/layout folder. It is constructed in 2 layer – VideoView in the back and PlaybackOverlayFragment in the front. VideoView is the view which we will play video contents, and PlaybackOverlayFragment will show the UI for controlling video, which we will focus on in this chapter. Implementat activity_playback_overlay.xml as follows.

<?xml version="1.0" encoding="utf-8"?>

<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_alignParentRight="true" android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true" android:layout_alignParentBottom="true"
        android:layout_height="match_parent" android:layout_gravity="center"
        android:layout_centerInParent="true"></VideoView>

    <fragment android:id="@+id/playback_controls_fragment"
        android:name="com.corochann.androidtvapptutorial.PlaybackOverlayFragment"
        android:layout_width="match_parent" android:layout_height="match_parent" />

</FrameLayout>

We don’t need any modification for PlaybackOverlayActivity for now.

PlaybackOverlayFragment

New -> Java Class -> Name: PlaybackOverlayFragment

This PlaybackOverlayFragment is subclass of android.support.v17.leanback.app.PlaybackOverlayFragment which provides us component to make video control UI.

It works very similar to VideoDetailsFragment, so you just need to call setAdapter(adapter), after setting rows in adapter. For PlaybackOverlayFragment, we need to set instance of “PlaybackControlsRow”, which shows the video control UI, to first row element of adapter.

PlaybackOverview1

So let’s study “PlaybackControlsRow” and its Presenter, “PlaybackControlsRowPresenter”. We need to specify following in each instance.

  • PlaybackControlsRow
    • PrimaryActionsAdapter      – It owns icons for main row
    • SecondaryActionsAdapter – It owns icons for sub rowNote that PlaybackControlsRow class provides us many useful default icons for video control. We only need to instantiate its inner class.
  • PlaybackControlsRowPresenter
    • DescriptionPresenter – It is Presenter for displaying item details on the top of PrimaryActions bar. 
PlaybackOverview2

Above photo explains internal construction of PlaybackControlsRow – PlaybackControlsRowpresenter. Constructor of PlaybackControlsRowPresenter takes argument of  DescriptionPresenter object, which determines how to show video details. This time, we will reuse DetailsDescriptionPresenter, which we made in previous chapter to show item details in DetailsFragment. PlaybackControlsRow have 2 rows inside to set the actions, PrimaryActionsAdapter and SecondaryActionsAdapter. We can set “actions” icon in these actionsadapters. See above photos for the available actions and these icons.

Sample implementation of PlaybackOverlayFragment is following.

package com.corochann.androidtvapptutorial;

import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
import android.util.Log;

/**
 * Created by corochann on 7/7/2015.
 */
public class PlaybackOverlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment {

    private static final String TAG = PlaybackOverlayFragment.class.getSimpleName();

    private Movie mSelectedMovie;
    private PlaybackControlsRow mPlaybackControlsRow;
    private ArrayObjectAdapter mPrimaryActionAdapter;
    private ArrayObjectAdapter mSecondaryActionAdapter;

    private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
    private PlaybackControlsRow.RepeatAction mRepeatAction;
    private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
    private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
    private PlaybackControlsRow.ShuffleAction mShuffleAction;
    private PlaybackControlsRow.SkipNextAction mSkipNextAction;
    private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
    private PlaybackControlsRow.FastForwardAction mFastForwardAction;
    private PlaybackControlsRow.RewindAction mRewindAction;
    private PlaybackControlsRow.HighQualityAction mHighQualityAction;
    private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction;
    private PlaybackControlsRow.MoreActions mMoreActions;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate");
        super.onCreate(savedInstanceState);

        mSelectedMovie = (Movie) getActivity().getIntent().getSerializableExtra(DetailsActivity.MOVIE);

        setBackgroundType(PlaybackOverlayFragment.BG_LIGHT);
        setFadingEnabled(true);

        setUpRows();
    }

    private ArrayObjectAdapter mRowsAdapter;

    private void setUpRows() {
        ClassPresenterSelector ps = new ClassPresenterSelector();

        PlaybackControlsRowPresenter playbackControlsRowPresenter;
        playbackControlsRowPresenter = new PlaybackControlsRowPresenter(new DetailsDescriptionPresenter());

        ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter);
        ps.addClassPresenter(ListRow.class, new ListRowPresenter());
        mRowsAdapter = new ArrayObjectAdapter(ps);

        /*
         * Add PlaybackControlsRow to mRowsAdapter, which makes video control UI.
         * PlaybackControlsRow is supposed to be first Row of mRowsAdapter.
         */
        addPlaybackControlsRow();
        /* add ListRow to second row of mRowsAdapter */
        addOtherRows();

        setAdapter(mRowsAdapter);

    }

    private void addPlaybackControlsRow() {
        mPlaybackControlsRow = new PlaybackControlsRow(mSelectedMovie);
        mRowsAdapter.add(mPlaybackControlsRow);

        ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector();
        mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
        mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
        mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter);
        mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter);
        
        Activity activity = getActivity();
        mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(activity);
        mRepeatAction = new PlaybackControlsRow.RepeatAction(activity);
        mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(activity);
        mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(activity);
        mShuffleAction = new PlaybackControlsRow.ShuffleAction(activity);
        mSkipNextAction = new PlaybackControlsRow.SkipNextAction(activity);
        mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(activity);
        mFastForwardAction = new PlaybackControlsRow.FastForwardAction(activity);
        mRewindAction = new PlaybackControlsRow.RewindAction(activity);
        mHighQualityAction = new PlaybackControlsRow.HighQualityAction(activity);
        mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(activity);
        mMoreActions = new PlaybackControlsRow.MoreActions(activity);

        /* PrimaryAction setting */
        mPrimaryActionsAdapter.add(mSkipPreviousAction);
        mPrimaryActionsAdapter.add(mRewindAction);
        mPrimaryActionsAdapter.add(mPlayPauseAction);
        mPrimaryActionsAdapter.add(mFastForwardAction);
        mPrimaryActionsAdapter.add(mSkipNextAction);

        /* SecondaryAction setting */
        mSecondaryActionsAdapter.add(mThumbsUpAction);
        mSecondaryActionsAdapter.add(mThumbsDownAction);
        mSecondaryActionsAdapter.add(mRepeatAction);
        mSecondaryActionsAdapter.add(mShuffleAction);
        mSecondaryActionsAdapter.add(mHighQualityAction);
        mSecondaryActionsAdapter.add(mClosedCaptioningAction);
        mSecondaryActionsAdapter.add(mMoreActions);
    }

    private void addOtherRows() {
        ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
        Movie movie = new Movie();
        movie.setTitle("Title");
        movie.setStudio("studio");
        movie.setDescription("description");
        movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg");
        listRowAdapter.add(movie);
        listRowAdapter.add(movie);

        HeaderItem header = new HeaderItem(0, "OtherRows");
        mRowsAdapter.add(new ListRow(header, listRowAdapter));
    }

}

To launch PlaybackOverlayAcitivity from DetailsActivity, let’s implement setOnActionClickedListener.

        @Override
        protected void onPostExecute(DetailsOverviewRow row) {
            /* 1st row: DetailsOverviewRow */

              /* action setting*/
            SparseArrayObjectAdapter sparseArrayObjectAdapter = new SparseArrayObjectAdapter();
            sparseArrayObjectAdapter.set(0, new Action(ACTION_PLAY_VIDEO, "Play Video"));
            sparseArrayObjectAdapter.set(1, new Action(1, "Action 2", "label"));
            sparseArrayObjectAdapter.set(2, new Action(2, "Action 3", "label"));

            row.setActionsAdapter(sparseArrayObjectAdapter);

            mFwdorPresenter.setOnActionClickedListener(new OnActionClickedListener() {
                @Override
                public void onActionClicked(Action action) {
                    if (action.getId() == ACTION_PLAY_VIDEO) {
                        Intent intent = new Intent(getActivity(), PlaybackOverlayActivity.class);
                        intent.putExtra("Movie", mSelectedMovie);
                        intent.putExtra("shouldStart", true);
                        startActivity(intent);
                    }
                }
            });
    <string name="movie">Movie</string>
    <string name="should_start">shouldStart</string>

Build and Run

PlaybackOverview1-2015-07-14-200711

Background VideoView is not implemented yet and black. But we can see Video control UI is already done!

Source code is on github.

I will continue to implement video controls using this UI in next chapter.

ErrorFragment
– Android TV app hands on Tutorial 6

ErrorFragment

ErrorActivity & ErrorFragment

ErrorFragment is one of another layouts supported by Leanback library.

ErrorActivity

New -> Java Class -> Name: ErrorActivity
* We don’t use layout file for showing ErrorActivity.

Basically, it just attaches ErrorFragment.

package com.corochann.androidtvapptutorial;

import android.app.Activity;
import android.os.Bundle;

/**
 * Created by corochann on 7/7/2015.
 */
public class ErrorActivity extends Activity {

    private static final String TAG = ErrorActivity.class.getSimpleName();

    private ErrorFragment mErrorFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        testError();
    }

    private void testError() {
        mErrorFragment = new ErrorFragment();
        getFragmentManager().beginTransaction().add(R.id.main_browse_fragment, mErrorFragment).commit();
    }
}

ErrorFragment

New -> Java Class -> Name: ErrorFragment
* We don’t use layout file for showing ErrorActivity.

ErrorFragment class is a subclass of android.support.v17.leanback.app.ErrorFragment.

package com.corochann.androidtvapptutorial;

import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * Modified from AOSP sample code by corochann on 7/7/2015.
 * This class demonstrates how to extend ErrorFragment
 */
public class ErrorFragment extends android.support.v17.leanback.app.ErrorFragment {

    private static final String TAG = ErrorFragment.class.getSimpleName();
    private static final boolean TRANSLUCENT = true;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        
        setTitle(getResources().getString(R.string.app_name));
        setErrorContent();
    }

    void setErrorContent() {
        setImageDrawable(getActivity().getDrawable(R.drawable.lb_ic_sad_cloud));
        setMessage(getResources().getString(R.string.error_fragment_message));
        setDefaultBackground(TRANSLUCENT);

        setButtonText(getResources().getString(R.string.dismiss_error));
        setButtonClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                getFragmentManager().beginTransaction().remove(ErrorFragment.this).commit();
            }
        });
    }
}

Modify AndroidManifest.xml to declare ErrorActivity,

    <application>

    ...

        <activity android:name=".ErrorActivity" />
    </application>


Implement action to launch ErrorActivity

I will change the name of item of GridItemPresenter “Item 1” -> “ErrorFragment”, and I will launch this ErrorActivity by clicking this “ErrorFragment” item.

Modify MainFragment, to launch ErrorActivity by intent.

    private void loadRows() {
        ...
        //gridRowAdapter.add("ITEM 1");
        gridRowAdapter.add("ErrorFragment");
        ...
    }
        @Override
        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
            // each time the item is clicked, code inside here will be executed.
            if (item instanceof Movie) {
                Movie movie = (Movie) item;
                Log.d(TAG, "Item: " + item.toString());
                Intent intent = new Intent(getActivity(), DetailsActivity.class);
                intent.putExtra(DetailsActivity.MOVIE, movie);

                getActivity().startActivity(intent);
            } else if (item instanceof String){
                if (item == "ErrorFragment") {
                    Intent intent = new Intent(getActivity(), ErrorActivity.class);
                    startActivity(intent);
                }
            }
        }
<resources>
    <string name="app_name">AndroidTVappTutorial</string>
    <string name="title_activity_main">MainActivity</string>

    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="title_activity_details">DetailsActivity</string>

    <string name="error">Error</string>
    <!-- Error messages -->
    <string name="video_error_media_load_timeout">Media loading timed out</string>
    <string name="video_error_server_inaccessible">Media server was not reachable</string>
    <string name="video_error_unknown_error">Failed to load video</string>
    <string name="error_fragment_message">An error occurred</string>
    <string name="dismiss_error">Dismiss</string>
    <string name="oops">Oops</string>
</resources>

Build and run

By clicking “ErrorFragment” item, ErrorFragment will show Error message with images.

ErrorFragment

 * AOSP project, SpinnerFragment is also implemented to show loading animation. But I introduced a minimum implementation/usage of ErrorFragment here.

Source code is on github.

DetailsOverviewRowPresenter & FullWidthDetailsOverviewRowPresenter
– Android TV application hands on tutorial 5

FullWidthDetailsOverviewRowPresenter1

[Update 2015.12.14]: revise & add DetailsOverviewRowPresenter sample implementation, see bottom of this post.

Aim of this chapter – DetailsActivity implementation

Implementing

  1. setOnItemViewClickedListener – onItemClicked callback function in MainFragment
    After implementation, we can click cards to go to next action. We show content detail information for each Movie item in this chapter.
  2. DetailsActivityVideoDetailsFragment, and DetailsDescriptionPresenter
    DetailsActivity is invoked by clicking card in MainActivity. It shows VideoDetailsFragment, which is a layout to show card content.

Implementing click listener in MainFragment

To register a next action when user clicked certain card, we can use setOnItemViewClickedListener method defined in BrowseFragment class (MainFragment class is a sub class of BrowseFragment). 

Sample implementation is as follows. This is almost same with setOnItemViewSelectedListener introduced in previous chapter.

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {

        ...

        setupEventListeners();

        picassoBackgroundManager = new PicassoBackgroundManager(getActivity());
    }

    private void setupEventListeners() {
        setOnItemViewSelectedListener(new ItemViewSelectedListener());
        setOnItemViewClickedListener(new ItemViewClickedListener());
    }

    private final class ItemViewClickedListener implements OnItemViewClickedListener {
        @Override
        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
            // each time the item is clicked, code inside here will be executed.

            }
        }
    }

DetailsActivity & VideoDetailsFragment – Explanation

Below is a picture from AOSP sample applicaton.  

DetailsFragment_combined_picture_explain

In the Android sample application, we have 2 rows in VideoDetailsFragment. First row is DetailsOverviewRow and second row is ListRow (which is already explained in MainFragment). DetailsOverviewRow shows the content details including picture in the lect, description and some actions are set in the left-bottom.

We can prepare our own Presenter to specify the design layout of DetailsOverviewRow. There are 2 pre-implemented presenters we can use in Leanback support library. 

  1. DetailsOverviewRowPresenter: Shown in the above picture, but it is already deprecated in leanback library version 22.2.0.
  2. FullWidthDetailsOverviewRowPresenter: Instead of DetailsOverviewRowPresenter, use this presenter is recommended in the AOSP document.

In the following, I will try to introduce this new FullWidthDetailsOverviewRowPresenter (*updated 2015.12.4: you can also check DetailsOverviewRowPresenter, see bottom of this post). It will specify the design layout of DetailsOverviewRow, which is usually used in the first row of your DetailsFragment to show item details information.

FullWidthDetailsOverviewRowPresenter is consisting of 3 parts, namely

  1. Logo view – customizable (option), by implementing DetailsOverViewLogoPresenter
  2. Action list view
  3. Detailed description view – customizable (MUST), implement subclass of AbstractDetailsDescriptionPresenter
FullWidthDetailsOverviewRowPresenter

We define “DetailsDescriptionPresenter” which extends AbstractDetailsDescriptionPresenter defined in Leanback libarary. AbstractDetailsDescriptionPresenter decides the design layout of descrption view.

DetailsActivity & VideoDetailsFragment – Implementation

We proceed to create DetailsActivity for showing the UI of content details. The design is specified in VideoDetailsFragment, which is a subclass of DetailsFragment.

Creating DetailsActivity & VideoDetailsFragment is done in the same way with MainActivity & MainFragment introduced in chapter 1.

DetailsActivity

New → Activity → BlankActivity

Activity Name: DetailsActivity
Layout Name: activity_details
Title: DetailsActivity
Menu Resource Name: menu_details
Hierarchical Parent: blank 

VideoDetailsFragment

New -> Java Class -> Name: VideoDetailsFragment

First, modify activity_details.xml as follows so that it only displays VideoDetailsFragment.

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:id="@+id/details_fragment"
    android:name="com.corochann.androidtvapptutorial.ui.VideoDetailsFragment"
    android:layout_width="match_parent" android:layout_height="match_parent"
    tools:context=".DetailsActivity" tools:deviceIds="tv" />

Second modify VideoDetailsFragment. We will make this VideoDetailsFragment as a sub-class of DetailsFragmentDetailsFragment class is in leanback support library to create UI for content details. In the VideoDetailsFragment, declared private member mFwdorPresenter is the instance of FullWidthDetailsOverviewRowPresenter.

Note that AsyncTask is for executing some tasks in background thread (“doInBackground“)  followed by executing some tasks in UI thread (“onPostExecute“). Here, we load a picture image in background, and updating UI in UI thread. You can find information of AsyncTask below.

package com.corochann.androidtvapptutorial;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.DetailsOverviewRow;
import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
import android.util.Log;

import com.squareup.picasso.Picasso;

import java.io.IOException;

/**
 * Created by corochann on 6/7/2015.
 */
public class VideoDetailsFragment extends DetailsFragment {

    private static final String TAG = VideoDetailsFragment.class.getSimpleName();

    private static final int DETAIL_THUMB_WIDTH = 274;
    private static final int DETAIL_THUMB_HEIGHT = 274;


    private static final String MOVIE = "Movie";

    private CustomFullWidthDetailsOverviewRowPresenter mFwdorPresenter;
    private PicassoBackgroundManager mPicassoBackgroundManager;

    private Movie mSelectedMovie;
    private DetailsRowBuilderTask mDetailsRowBuilderTask;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate");
        super.onCreate(savedInstanceState);

        mFwdorPresenter = new CustomFullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter());

        mPicassoBackgroundManager = new PicassoBackgroundManager(getActivity());
        mSelectedMovie = (Movie)getActivity().getIntent().getSerializableExtra(MOVIE);

        mDetailsRowBuilderTask = (DetailsRowBuilderTask) new DetailsRowBuilderTask().execute(mSelectedMovie);
        mPicassoBackgroundManager.updateBackgroundWithDelay(mSelectedMovie.getCardImageUrl());;
    }

    @Override
    public void onStop() {
        mDetailsRowBuilderTask.cancel(true);
        super.onStop();
    }

    private class DetailsRowBuilderTask extends AsyncTask<Movie, Integer, DetailsOverviewRow> {
        @Override
        protected DetailsOverviewRow doInBackground(Movie... params) {
            DetailsOverviewRow row = new DetailsOverviewRow(mSelectedMovie);
            try {
                Bitmap poster = Picasso.with(getActivity())
                        .load(mSelectedMovie.getCardImageUrl())
                        .resize(Utils.convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_WIDTH),
                                Utils.convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_HEIGHT))
                        .centerCrop()
                        .get();
                row.setImageBitmap(getActivity(), poster);
            } catch (IOException e) {
                Log.w(TAG, e.toString());
            }


            return row;
        }

        @Override
        protected void onPostExecute(DetailsOverviewRow row) {
                    /* 1st row: DetailsOverviewRow */
            SparseArrayObjectAdapter sparseArrayObjectAdapter = new SparseArrayObjectAdapter();
            for (int i = 0; i<10; i++){
                sparseArrayObjectAdapter.set(i, new Action(i, "label1", "label2"));
            }
            row.setActionsAdapter(sparseArrayObjectAdapter);

        /* 2nd row: ListRow */
            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter());
            for(int i = 0; i < 10; i++){
                Movie movie = new Movie();
                if(i%3 == 0) {
                    movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg");
                } else if (i%3 == 1) {
                    movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02630.jpg");
                } else {
                    movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02529.jpg");
                }
                movie.setTitle("title" + i);
                movie.setStudio("studio" + i);
                listRowAdapter.add(movie);
            }
            HeaderItem headerItem = new HeaderItem(0, "Related Videos");

            ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector();
            mFwdorPresenter.setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_SMALL);
            Log.e(TAG, "mFwdorPresenter.getInitialState: " +mFwdorPresenter.getInitialState());

            classPresenterSelector.addClassPresenter(DetailsOverviewRow.class, mFwdorPresenter);
            classPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());

            ArrayObjectAdapter adapter = new ArrayObjectAdapter(classPresenterSelector);
            /* 1st row */
            adapter.add(row);
            /* 2nd row */
            adapter.add(new ListRow(headerItem, listRowAdapter));
            /* 3rd row */
            //adapter.add(new ListRow(headerItem, listRowAdapter));
            setAdapter(adapter);

        }
    }
}

Note that constructor of adapter is defferent between MainFragment and VideoDetailsFragment. We are only using ListRow – ListRowPresenter in the MainFragment. In that case we can instantiate adapter by setting Presenter itself like 

adapter = new ArrayObjectAdapter(new ListRowPresenter());

However, we are using DetailsOverviewRow – FullWidthDetailsOverviewRowPresenter & ListRow – ListRowPresenter in VideoDetails Fragment. ClassPresenterSelector defines this correspondence, and we can use it in the argument of constructor of adapter. 

ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector();
classPresenterSelector.addClassPresenter(DetailsOverviewRow.class, mFwdorPresenter);
classPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());
adapter = new ArrayObjectAdapter(classPresenterSelector);

Next, add description member in Movie class, you can implement Getter and Setter method by [Alt]+[Insert] in Android studio. Also, make Movie Serializable so that we can pass this object through intent. Because we pass Movie object through intent from MainActivity to DetailsActivity

* See also: Fast, easy Parcelable implementation with Android studio Parcelable plugin for better performance implementation.

public class Movie implements Serializable {

    private static final String TAG = Movie.class.getSimpleName();

    static final long serialVersionUID = 727566175075960653L;
    private long id;
    private String title;
    private String studio;
    private String description;
    private String cardImageUrl;

    ...

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    ...

}

Copy DetailsDescriptionPresenter from AOSP sample source code as follows. Again, DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter, which decides the design layout of descrption view.

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.corochann.androidtvapptutorial;

import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;

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());
        }

    }
}

Finally, modify MainFragment to send intent to launch DetailsActivity.

    private void setupEventListeners() {
        setOnItemViewSelectedListener(new ItemViewSelectedListener());
        setOnItemViewClickedListener(new ItemViewClickedListener());
    }

    private final class ItemViewClickedListener implements OnItemViewClickedListener {
        @Override
        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
            // each time the item is clicked, code inside here will be executed.
            if (item instanceof Movie) {
                Movie movie = (Movie) item;
                Log.d(TAG, "Item: " + item.toString());
                Intent intent = new Intent(getActivity(), DetailsActivity.class);
                intent.putExtra(DetailsActivity.MOVIE, movie);

                getActivity().startActivity(intent);
            }
        }
    }

Build and Run!

Now you can see the content details by clicking card. 

Initial Display, Logo is on the left, background is on the top half, and description is on the bottom half.

When user press “down” key, description view will take full screen.

FullWidthDetailsOverviewRowPresenter2

When user press “down” key again, next row (ListRow in this example) will appear. 

FullWidthDetailsOverviewRowPresenter3

Source code is on github.

——- Update: 2015.12.14 Add DetailsOverviewRowPresenter implementation —————-

Build and Run 2!

detailsoverviewrowpresenter01
DetailsOverviewRowPresenter
fullwidthdetailsoverviewrowpresenter01
FullWidthDetailsOverviewRowPresenter

Please check updated source code on github if you are interested in (already deprecated) DetailsOverviewRowPresenter implementation. Why I updated this code? Because it seems still some developer prefers DetailsOverviewRowPresenter design, even if it was deprecated!

PicassoBackgroundManager
– Android TV application hands on tutorial 4

[Updated 2015.11.18]: Revise.
[Updated 2016.3.13]: Updated wrong source code.

Aim of this chapter

Implementing background image update feature. Application was boring with no background, and it becomes much nice if it has appropriate background.
Just setting background is easy, though, Android TV sample application explains how to dynamically change background linking with your current selection of contents.

Before explanation of background change, I start explanation of onItemSelected callback function so that we get the event notification when the item is selected. Next, I will show simple background change implementation followed by better performance implementation using Picasso library.

setOnItemViewSelectedListener listener & onItemSelected callback

BrowseFragment supports to set listener when the itemview is selected & clicked. Current target is to be get notified when the user move the cursor and change the selection of item.

we can use setOnItemViewSelectedListener(OnItemViewSelectedListener listener) function for this purpose. In the argument, you can put the listener class which should implement OnItemViewSelectedListener interface which is also provided by leanback library. Then, you can implement onItemSelected callback function, which is the function called when an item is selected.

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.i(TAG, "onActivityCreated");
        super.onActivityCreated(savedInstanceState);

        setupUIElements();

        loadRows();

        setupEventListeners();
    }
    
    private void setupEventListeners() {
        setOnItemViewSelectedListener(new ItemViewSelectedListener());
    }
    
    private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
        @Override
        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 
                                   RowPresenter.ViewHolder rowViewHolder, Row row) {
            // each time the item is selected, code inside here will be executed.
        }
    }

We will proceed to implement background change function in the following. Here I arranged to create SimpleBackgroundManager, and PicassoBackgroundManager to handle background image (Android TV sample application is doing it inside MainFragment.java).

SimpleBackgroundManager

There is explanation in official developer’s site, see Update the Background for reference.
I wrote some test code below.

Right click package name → New → class → SimpleBackgroundManager

This SimpleBackgroundManager keeps a member mBackgroundManager of BackgroundManager class, which handles the actual background change. This BackgroundManager instance is a singleton instance which can be obtained via BackgroundManager.getInstance().

package com.corochann.androidtvapptutorial;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.v17.leanback.app.BackgroundManager;
import android.util.DisplayMetrics;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URI;


/**
 * Created by corochann on 3/7/2015.
 */
public class SimpleBackgroundManager {

    private static final String TAG = SimpleBackgroundManager.class.getSimpleName();

    private final int DEFAULT_BACKGROUND_RES_ID = R.drawable.default_background;
    private static Drawable mDefaultBackground;

    private Activity mActivity;
    private BackgroundManager mBackgroundManager;

    public SimpleBackgroundManager(Activity activity) {
        mActivity = activity;
        mDefaultBackground = activity.getDrawable(DEFAULT_BACKGROUND_RES_ID);
        mBackgroundManager = BackgroundManager.getInstance(activity);
        mBackgroundManager.attach(activity.getWindow());
        activity.getWindowManager().getDefaultDisplay().getMetrics(new DisplayMetrics());
    }

    public void updateBackground(Drawable drawable) {
        mBackgroundManager.setDrawable(drawable);
    }

    public void clearBackground() {
        mBackgroundManager.setDrawable(mDefaultBackground);
    }

}

At first, instance of BackgroundManager is created in Constructor. It must be attached with Window before updating background, and these initialization is done in constructor. 

updateBackground method will change the background, and clearBackground method will update the background to default image. (I have added the res/drawable/default_background.xml and updated res/values/colors.xml.)

Modification of MainFragment is small.

public class MainFragment extends BrowseFragment {

    ...

    private static SimpleBackgroundManager simpleBackgroundManager = null;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {

        ...

        simpleBackgroundManager = new SimpleBackgroundManager(getActivity());
    }

    private void setupEventListeners() {
        setOnItemViewSelectedListener(new ItemViewSelectedListener());
    }

    private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
        @Override
        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
                                   RowPresenter.ViewHolder rowViewHolder, Row row) {
            // each time the item is selected, code inside here will be executed.
            if (item instanceof String) { // GridItemPresenter row
                simpleBackgroundManager.clearBackground();
            } else if (item instanceof Movie) { // CardPresenter row
                simpleBackgroundManager.updateBackground(getActivity().getDrawable(R.drawable.movie));
            }
        }
    }

Build and run!

We can check background is updated depending on the selection of the row. Also the Background will be back to default when you go back to GridItemPresenter row.

background1

Source code is on github.

PicassoBackgroundManager

Let’s improve SimpleBackgroundManager implementation. What we will improve is following.

  1. Delay updating background
    In the previous implementation, main thread will always try to update background when user is moving their cursor, and changing select item. It is busy, and may cause bad performance. Below we will implement TimerTask to wait certain period of time from updating background image.
  2. Image handling by using Picasso library
    Picasso library is “A powerful image downloading and caching library for Android“. We will use it for more easier image resource handling.

Create new class PicassoBackgroundManager, implement as follows

package com.corochann.androidtvapptutorial;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.support.v17.leanback.app.BackgroundManager;
import android.util.DisplayMetrics;
import android.util.Log;

import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Created by corochann on 3/7/2015.
 */
public class PicassoBackgroundManager {

    private static final String TAG = PicassoBackgroundManager.class.getSimpleName();

    private static int BACKGROUND_UPDATE_DELAY = 500;
    private final int DEFAULT_BACKGROUND_RES_ID = R.drawable.default_background;
    private static Drawable mDefaultBackground;
    // Handler attached with main thread
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    private Activity mActivity;
    private BackgroundManager mBackgroundManager = null;
    private DisplayMetrics mMetrics;
    private URI mBackgroundURI;
    private PicassoBackgroundManagerTarget mBackgroundTarget;

    Timer mBackgroundTimer; // null when no UpdateBackgroundTask is running.

    public PicassoBackgroundManager (Activity activity) {
        mActivity = activity;
        mDefaultBackground = activity.getDrawable(DEFAULT_BACKGROUND_RES_ID);
        mBackgroundManager = BackgroundManager.getInstance(activity);
        mBackgroundManager.attach(activity.getWindow());
        mBackgroundTarget = new PicassoBackgroundManagerTarget(mBackgroundManager);
        mMetrics = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);

    }

    /**
     * if UpdateBackgroundTask is already running, cancel this task and start new task.
     */
    private void startBackgroundTimer() {
        if (mBackgroundTimer != null) {
            mBackgroundTimer.cancel();
        }
        mBackgroundTimer = new Timer();
        /* set delay time to reduce too much background image loading process */
        mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
    }


    private class UpdateBackgroundTask extends TimerTask {
        @Override
        public void run() {
            /* Here is TimerTask thread, not UI thread */
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                     /* Here is main (UI) thread */
                    if (mBackgroundURI != null) {
                        updateBackground(mBackgroundURI);
                    }
                }
            });
        }
    }

    public void updateBackgroundWithDelay(String url) {
        try {
            URI uri = new URI(url);
            updateBackgroundWithDelay(uri);
        } catch (URISyntaxException e) {
            /* skip updating background */
            Log.e(TAG, e.toString());
        }
    }

    /**
     * updateBackground with delay
     * delay time is measured in other Timer task thread.
     * @param uri
     */
    public void updateBackgroundWithDelay(URI uri) {
        mBackgroundURI = uri;
        startBackgroundTimer();
    }

    private void updateBackground(URI uri) {
        try {
            Picasso.with(mActivity)
                    .load(uri.toString())
                    .resize(mMetrics.widthPixels, mMetrics.heightPixels)
                    .centerCrop()
                    .error(mDefaultBackground)
                    .into(mBackgroundTarget);
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
    }

    /**
     * Copied from AOSP sample code.
     * Inner class
     * Picasso target for updating default_background images
     */
    public class PicassoBackgroundManagerTarget implements Target {
        BackgroundManager mBackgroundManager;

        public PicassoBackgroundManagerTarget(BackgroundManager backgroundManager) {
            this.mBackgroundManager = backgroundManager;
        }

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
            this.mBackgroundManager.setBitmap(bitmap);
        }

        @Override
        public void onBitmapFailed(Drawable drawable) {
            this.mBackgroundManager.setDrawable(drawable);
        }

        @Override
        public void onPrepareLoad(Drawable drawable) {
            // Do nothing, default_background manager has its own transitions
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            PicassoBackgroundManagerTarget that = (PicassoBackgroundManagerTarget) o;

            if (!mBackgroundManager.equals(that.mBackgroundManager))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            return mBackgroundManager.hashCode();
        }
    }

    
}

Now, we will replace from SimpleBackgroundManager to PicassoBackgroundManager in MainFragment.java

public class MainFragment extends BrowseFragment {

    ...

    private static PicassoBackgroundManager picassoBackgroundManager = null;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {

        ...

        picassoBackgroundManager = new PicassoBackgroundManager(getActivity());
    }

    private void setupEventListeners() {
        setOnItemViewSelectedListener(new ItemViewSelectedListener());
    }

    private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
        @Override
        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
                                   RowPresenter.ViewHolder rowViewHolder, Row row) {
            if (item instanceof String) {                    // GridItemPresenter
                picassoBackgroundManager.updateBackgroundWithDelay("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/10/RIMG0656.jpg");
            } else if (item instanceof Movie) {              // CardPresenter
                picassoBackgroundManager.updateBackgroundWithDelay(((Movie) item).getCardImageUrl());
            }
        }
    }

    ...

    private void loadRows() {
        mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());

        ...

        for(int i=0; i<10; i++) {
            Movie movie = new Movie();
            if(i%3 == 0) {
                movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg");
            } else if (i%3 == 1) {
                movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02630.jpg");
            } else {
                movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02529.jpg");
            }
            movie.setTitle("title" + i);
            movie.setStudio("studio" + i);
            cardRowAdapter.add(movie);
        }

Build and run!

We can check background is updated after 500 ms thanks to the timer task. And we are getting the background image from web by using Picasso.

Source code is on github.

Until now, we cannot click item/card. Next chapter, DetailsOverviewRowPresenter & FullWidthDetailsOverviewRowPresenter – Android TV application hands on tutorial 5, is to implement a onClickListener and show content details by DetailFragment.

How to use Presenter and ViewHolder?
– Android TV application hands on tutorial 3

picasso-image-download

[Update 2015.11.18]: Revise.

Aim of this chapter

In previous chapter, we looked GridItemPresenter. Its relationship was following.

  • Presenter: GridItemPresenter
  • ViewHolder’s view: TextView
  • CardInfo/Item: String

This was easy example. In this chapter, we proceed to introduce another type of Presenter,

  • Presenter: CardPresenter
  • ViewHolder’s view: ImageCardView
  • CardInfo/Item: Movie class

ImageCardView

ImageCardView class is provided from Android SDK, and it provides a card design layout with main image, title text and content text.

ImageCardView is a subclass of BaseCardView, so it is nice to look BaseCardView class. This is the explanation of BaseCardView,

android.support.v17.leanback.widget
public class BaseCardView
extends android.widget.FrameLayout
A card style layout that responds to certain state changes. It arranges its children in a vertical column, with different regions becoming visible at different times.
A BaseCardView will draw its children based on its type, the region visibilities of the child types, and the state of the widget. A child may be marked as belonging to one of three regions: main, info, or extra. The main region is always visible, while the info and extra regions can be set to display based on the activated or selected state of the View. The card states are set by calling setActivated and setSelected.

BaseCardView itself does not provide specific design layout. So when you want to utilize this, you can make subclass of BaseCardView which have specific design. ImageCardView is one of the class, and currently I could find only ImageCardView class as the subclass of BaseCardView provided by SDK.

In this chapter, we will add this ImageCardView to our code.

Implement CardPresenter, Movie class

I will start by placing necessary files at first. Rightclick on package,

  1. New → class → CardPresenter
  2. New → class → Movie 
  3. For the main image, we use movie.png.
    Copy res/drawable/movie.png from Android TV sample application.
  4. We will use Utility functions provided by Android TV sample application.
    Copy [package name]/Utils class from Android TV sample application to your source code.

First, Utils.java is just copying from AOSP, which will be below.

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.corochann.androidtvapptutorial;

import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;

/**
 * A collection of utility methods, all static.
 */
public class Utils {

    /*
     * Making sure public utility methods remain static
     */
    private Utils() {
    }

    /**
     * Returns the screen/display size
     */
    public static Point getDisplaySize(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        return size;
    }

    /**
     * Shows a (long) toast
     */
    public static void showToast(Context context, String msg) {
        Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
    }

    /**
     * Shows a (long) toast.
     */
    public static void showToast(Context context, int resourceId) {
        Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show();
    }

    public static int convertDpToPixel(Context ctx, int dp) {
        float density = ctx.getResources().getDisplayMetrics().density;
        return Math.round((float) dp * density);
    }

    /**
     * Formats time in milliseconds to hh:mm:ss string format.
     */
    public static String formatMillis(int millis) {
        String result = "";
        int hr = millis / 3600000;
        millis %= 3600000;
        int min = millis / 60000;
        millis %= 60000;
        int sec = millis / 1000;
        if (hr > 0) {
            result += hr + ":";
        }
        if (min >= 0) {
            if (min > 9) {
                result += min + ":";
            } else {
                result += "0" + min + ":";
            }
        }
        if (sec > 9) {
            result += sec;
        } else {
            result += "0" + sec;
        }
        return result;
    }
}

Second, Movie class defines the CardInfo/Item which CardPresenter will present using ImageCardView. It should have the imformation of

  • main image
  • title text
  • content text (studio)

But for the first stage, I only put the information of “title” and “content (studio)”.

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.corochann.androidtvapptutorial;

import android.util.Log;

import java.net.URI;
import java.net.URISyntaxException;

/**
 *  Modified from AOSP sample source code, by corochann on 2/7/2015.
 *  Movie class represents video entity with title, description, image thumbs and video url.
 */
public class Movie {

    private static final String TAG = Movie.class.getSimpleName();

    static final long serialVersionUID = 727566175075960653L;
    private long id;
    private String title;
    private String studio;

    public Movie() {
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getStudio() {
        return studio;
    }

    public void setStudio(String studio) {
        this.studio = studio;
    }

    @Override
    public String toString() {
        return "Movie{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

Last implementaiton is CardPresenter, it is a subclass of PresenterCardPresenter owns ViewHolder extended from parent’s Presenter.ViewHolder. This ViewHolder holds ImageCardView which is used to present UI for the Movie item. 

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.corochann.androidtvapptutorial;


import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Modified from AOSP sample source code, by corochann on 2/7/2015.
 */
public class CardPresenter extends Presenter {

    private static final String TAG = CardPresenter.class.getSimpleName();

    private static Context mContext;
    private static int CARD_WIDTH = 313;
    private static int CARD_HEIGHT = 176;

    static class ViewHolder extends Presenter.ViewHolder {
        private Movie mMovie;
        private ImageCardView mCardView;
        private Drawable mDefaultCardImage;

        public ViewHolder(View view) {
            super(view);
            mCardView = (ImageCardView) view;
            mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie);
        }

        public void setMovie(Movie m) {
            mMovie = m;
        }

        public Movie getMovie() {
            return mMovie;
        }

        public ImageCardView getCardView() {
            return mCardView;
        }

        public Drawable getDefaultCardImage() {
            return mDefaultCardImage;
        }

    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent) {
        Log.d(TAG, "onCreateViewHolder");
        mContext = parent.getContext();

        ImageCardView cardView = new ImageCardView(mContext);
        cardView.setFocusable(true);
        cardView.setFocusableInTouchMode(true);
        cardView.setBackgroundColor(mContext.getResources().getColor(R.color.fastlane_background));
        return new ViewHolder(cardView);
    }

    @Override
    public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
        Movie movie = (Movie) item;
        ((ViewHolder) viewHolder).setMovie(movie);

        Log.d(TAG, "onBindViewHolder");
        ((ViewHolder) viewHolder).mCardView.setTitleText(movie.getTitle());
        ((ViewHolder) viewHolder).mCardView.setContentText(movie.getStudio());
        ((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
        ((ViewHolder) viewHolder).mCardView.setMainImage(((ViewHolder) viewHolder).getDefaultCardImage());
    }

    @Override
    public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
        Log.d(TAG, "onUnbindViewHolder");
    }

    @Override
    public void onViewAttachedToWindow(Presenter.ViewHolder viewHolder) {
        // TO DO
    }

}

Preparation of data model = Movie and presenter = CardPresenter are done. We can show Movie item by putting the item to adapter.

private void loadRows() {
        mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
        ...

        /* CardPresenter */
        HeaderItem cardPresenterHeader = new HeaderItem(1, "CardPresenter");
        CardPresenter cardPresenter = new CardPresenter();
        ArrayObjectAdapter cardRowAdapter = new ArrayObjectAdapter(cardPresenter);

        for(int i=0; i<10; i++) {
            Movie movie = new Movie();
            movie.setTitle("title" + i);
            movie.setStudio("studio" + i);
            cardRowAdapter.add(movie);
        }
        mRowsAdapter.add(new ListRow(cardPresenterHeader, cardRowAdapter));

         ...
    }

Build and Run 1

cardpresenter2

cardpresenter3

CardPresenter header will appear in the second line, and ImageCardView shows the default card image. The title and content text will appear when you move from header to contents (when items are “onActivated”).

Source code is on github.

Updating main image after downloading picture from web using Picasso

Above example shows the default image in the ImageCardView which must be included together with your app (image is static). Sometimes, however, you want to use the image downloading from web so that your application can show updated information.

Picasso image loader library will help us to achieve this easily. Here are the references.

In the CardPresenter class, we want to use picasso library, which can be included by adding a following line in app/build.gradle file.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.android.support:leanback-v17:22.2.0'
    compile 'com.squareup.picasso:picasso:2.3.2'
}

We will add cardImageUrl member to Movie class, which points a URL for the main image. 

    private String cardImageUrl;

    public String getCardImageUrl() {
        return cardImageUrl;
    }

    public void setCardImageUrl(String cardImageUrl) {
        this.cardImageUrl = cardImageUrl;
    }

As your tips, getter and setter can be automatically generated by Android studio. In the above modification, you just need to declare cardImageUrl member followed by [Alt]+[Insert] and generate getter and setter. See How to automatically generate getters and setters in Android Studio. We also implement a getImageURI function, to convert URL string to URI format.

    public URI getCardImageURI() {
        try {
            return new URI(getCardImageUrl());
        } catch (URISyntaxException e) {
            return null;
        }
    }

CardPresenter takes care of updating image using picasso. This is done by implementing updateCardViewImage function. Picasso makes a source code intuitive to understand for loading, transforming image.

    public ViewHolder(View view) {
        super(view);
        mCardView = (ImageCardView) view;
        mImageCardViewTarget = new PicassoImageCardViewTarget(mCardView);
        mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie);
    }

       ...

    protected void updateCardViewImage(URI uri) {
        Picasso.with(mContext)
                .load(uri.toString())
                .resize(Utils.convertDpToPixel(mContext, CARD_WIDTH),
                        Utils.convertDpToPixel(mContext, CARD_HEIGHT))
                .error(mDefaultCardImage)
                .into(mImageCardViewTarget);
    }
}


...

@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
    Movie movie = (Movie) item;
    ((ViewHolder) viewHolder).setMovie(movie);

    Log.d(TAG, "onBindViewHolder");
    if (movie.getCardImageUrl() != null) {
        ((ViewHolder) viewHolder).mCardView.setTitleText(movie.getTitle());
        ((ViewHolder) viewHolder).mCardView.setContentText(movie.getStudio());
        ((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
        ((ViewHolder) viewHolder).updateCardViewImage(movie.getCardImageURI());
        //((ViewHolder) viewHolder).mCardView.setMainImage(((ViewHolder) viewHolder).getDefaultCardImage());
    }
}

At the last line of updateCardViewImage it calls into(mImageCardViewTarget)  method to load the image to imageview. This target is implemented as follows.

    public static class PicassoImageCardViewTarget implements Target {
        private ImageCardView mImageCardView;

        public PicassoImageCardViewTarget(ImageCardView imageCardView) {
            mImageCardView = imageCardView;
        }

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
            Drawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
            mImageCardView.setMainImage(bitmapDrawable);
        }

        @Override
        public void onBitmapFailed(Drawable drawable) {
            mImageCardView.setMainImage(drawable);
        }

        @Override
        public void onPrepareLoad(Drawable drawable) {
            // Do nothing, default_background manager has its own transitions
        }
    }

Interface Target is defined in picasso library, it 

represents an arbitrary listener for image loading.

Target interface allows us to implement 3 listener functions.

  • onBitmapLoaded  
    –  Callback when an image has been successfully loaded.
  • onBitmapFailed    
    – Callback when an image has been successfully loaded. linked with error()
  • onPrepareLoad    
    – Callback invoked right before your request is submitted. linked with placeholder()

Remaining task is to specify cardImageUrl from MainFragment, which is done in 

    private void loadRows() {

        ...

        for(int i=0; i<10; i++) {
            Movie movie = new Movie();
            movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg");
            movie.setTitle("title" + i);
            movie.setStudio("studio" + i);
            cardRowAdapter.add(movie);
        }
         ...
    }

At last, you need to add permission to use Internet in AndroidManifest.xml before building.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.corochann.androidtvapptutorial" >

    <uses-permission android:name="android.permission.INTERNET" />

    ...

Build and Run 2

picasso-image-download

Now main image is downloaded from the internet. 

Source code is on github.

Customizing ImageCardView, BaseCardView

We can change the design type, and the animation behavior of the card. To begin with, I recommend to refer BaseCardView explanation in source code provided by Android SDK,

A BaseCardView will draw its children based on its type, the region visibilities of the child types, and the state of the widget. A child may be marked as belonging to one of three regions: maininfo, or extra. The main region is always visible, while the info and extra regions can be set to display based on the activated or selected state of the View. The card states are set by calling setActivated and setSelected.

In BaseCardView class, you can check the options available to change the design.

  1. public void setCardType(int type)
  2. public void setInfoVisibility(int visibility)
  3. public void setExtraVisibility(int visibility)

setCardType(int type)

You can use following card type as argument

  • CARD_TYPE_MAIN_ONLY
  • CARD_TYPE_INFO_OVER
  • CARD_TYPE_INFO_UNDER
  • CARD_TYPE_INFO_UNDER_WITH_EXTRA

The example with ImageCardView

CARD_TYPE_MAIN_ONLY
CARD_TYPE_MAIN_ONLY
CARD_TYPE_INFO_OVER
CARD_TYPE_INFO_OVER
CARD_TYPE_INFO_UNDER
CARD_TYPE_INFO_UNDER, CARD_TYPE_INFO_UNDER_WITH_EXTRA

You can check the layout of ImageCardView in SDK folder, \sdk\extras\android\support\v17\leanback\res\layout\lb_image_card_view.xml.

ImageCardView has imageView as main region, and title and content text are in info region. The extra region is not set, therefore the behavior is same between CARD_TYPE_INFO_UNDER and CARD_TYPE_INFO_UNDER_WITH_EXTRA.

setInfoVisibility(int visibility), setExtraVisibility(int visibility)

You can use following card type as argument

  • CARD_REGION_VISIBLE_ALWAYS
    – the region (title & content text area) will always appear.
  • CARD_REGION_VISIBLE_ACTIVATED
    – the region will not appear when user is selecting header.
       the region will appear when user move to RowsFragment.
  • CARD_REGION_VISIBLE_SELECTED
    – the region will not appear when this card/item is not selected.
       the region will appear only when the card/item is selected.

The more detail explanation of these options can be referred from SDK source code.

Here I changed the setting by modifying onCreateViewHolder in CardPresenter class, 

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent) {
        Log.d(TAG, "onCreateViewHolder");
        mContext = parent.getContext();

        ImageCardView cardView = new ImageCardView(mContext);
        cardView.setCardType(BaseCardView.CARD_TYPE_INFO_UNDER);
        cardView.setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS);
        cardView.setFocusable(true);
        cardView.setFocusableInTouchMode(true);
        cardView.setBackgroundColor(mContext.getResources().getColor(R.color.fastlane_background));
        return new ViewHolder(cardView);
    }

Source code is on github.

Next chapter, PicassoBackgroundManager – Android TV application hands on tutorial 4, is implementing background image update feature.