Recommendation card
– Android TV app Tutorial hands on 11

recommendation

Recommendation

Android TV’s home, LeanbackLauncher has a recommendation row at first row. Any application can suggest recommended contents for users. In this chapter, I will explain how to show recommendation card to LeanbackLauncher from your application.

This recommendation is achieved by using Notification framework whose structure was already existed in Android phone/tablet SDK. So showing recommendation in LeanbackLauncher is actually sending notification. Basic sequence is following 

  1. Declare NotificationManager
  2. Use your customized RecommendationBuilder class (it is custom class which usesNotificationCompat class)  to prepare recommendation.
  3. Make Notification by building RecommendationBuilder
  4. Notify this Notification using NotificationManager.

What is implemented in this chapter?

2 new class, RecommendationBuilder & RecommendationFactory, are implemented in this chapter. RecommendationBuilder is the custom class to create notification for your application. RecommendationFactory actually create notification using RecommendationBuilder.

In this chapter, we will send notification (make recommendation) by clicking the button in MainFragment. It will invoke recommend method in RecommendationFactory to achive recommendation. (As written later, recommendation usually should be done in service. However I implemented recommendation in onClick for easy understanding.) 

RecommendationBuilder

First, RecommendationBuilder is implemented as follows.

package com.corochann.androidtvapptutorial;

import android.app.Notification;
import android.app.PendingIntent;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/*
 * This class builds recommendations as notifications with videos as inputs.
 */
public class RecommendationBuilder {

    private static final String TAG = RecommendationBuilder.class.getSimpleName();
    private static final String BACKGROUND_URI_PREFIX = "content://com.corochann.androidtvapptutorial/";

    private Context mContext;

    private int mId;
    private int mPriority;
    private int mFastLaneColor;
    private int mSmallIcon;
    private String mTitle;
    private String mDescription;
    private Bitmap mCardImageBitmap;
    private String mBackgroundUri;
    private Bitmap mBackgroundBitmap;
    private String mGroupKey;
    private String mSort;
    private PendingIntent mIntent;


    public RecommendationBuilder(Context context) {
        mContext = context;
        // default fast lane color
        setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
    }

    public RecommendationBuilder setFastLaneColor(int color) {
        mFastLaneColor = color;
        return this;
    }

    /* context must not be null. It should be specified in constructor */
/*
    public RecommendationBuilder setContext(Context context) {
        mContext = context;
        return this;
    }
*/

    public RecommendationBuilder setId(int id) {
        mId = id;
        return this;
    }

    public RecommendationBuilder setPriority(int priority) {
        mPriority = priority;
        return this;
    }

    public RecommendationBuilder setTitle(String title) {
        mTitle = title;
        return this;
    }

    public RecommendationBuilder setDescription(String description) {
        mDescription = description;
        return this;
    }

    public RecommendationBuilder setBitmap(Bitmap bitmap) {
        mCardImageBitmap = bitmap;
        return this;
    }

    public RecommendationBuilder setBackground(String uri) {
        mBackgroundUri = uri;
        return this;
    }

    public RecommendationBuilder setBackground(Bitmap bitmap) {
        mBackgroundBitmap = bitmap;
        return this;
    }

    public RecommendationBuilder setIntent(PendingIntent intent) {
        mIntent = intent;
        return this;
    }

    public RecommendationBuilder setSmallIcon(int resourceId) {
        mSmallIcon = resourceId;
        return this;
    }

    public Notification build() {

        Bundle extras = new Bundle();
        File bitmapFile = getNotificationBackground(mContext, mId);

        if (mBackgroundBitmap != null) {
            Log.d(TAG, "making URI for mBackgroundBitmap");
            extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
                    Uri.parse(BACKGROUND_URI_PREFIX + Integer.toString(mId)).toString());
        } else {
            Log.w(TAG, "mBackgroundBitmap is null");
        }

        // the following simulates group assignment into "Top", "Middle", "Bottom"
        // by checking mId and similarly sort order
        mGroupKey = (mId < 3) ? "Group1" : (mId < 5) ? "Group2" : "Group3";
        mSort = (mId < 3) ? "1.0" : (mId < 5) ? "0.7" : "0.3";

        // save bitmap into files for content provider to serve later
        try {
            bitmapFile.createNewFile();
            FileOutputStream fOut = new FileOutputStream(bitmapFile);
            mBackgroundBitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut); //

You can use this RecommendationBuilder class as a library. In that case you don’t need to know the detail of this implementation, so you can skip reading this section. Explanation about this class is written in the remaining of this section.

Most important part is build() function, which shows how to make Notification instance for Android TV recommendation. Recommendation in Android TV  is indeed Notification! It is making a instance of Notification class, by using NotificationCompat.BigPictureStyle method.

    public Notification build() {

        ...

        Notification notification = new NotificationCompat.BigPictureStyle(
                new NotificationCompat.Builder(mContext)
                        .setAutoCancel(true)
                        .setContentTitle(mTitle)
                        .setContentText(mDescription)
                        .setPriority(mPriority)
                        .setLocalOnly(true)
                        .setOngoing(true)
                        .setGroup(mGroupKey)
                        .setSortKey(mSort)
                        .setColor(mContext.getResources().getColor(R.color.fastlane_background))
                        .setCategory(Notification.CATEGORY_RECOMMENDATION)
                        .setLargeIcon(mCardImageBitmap)
                        .setSmallIcon(mSmallIcon)
                        .setContentIntent(mIntent)
                        .setExtras(extras))
                .build();

        Log.d(TAG, "Building notification - " + this.toString());

        return notification;
    }

Many of the function and its related part in the recommendation card is straightforward.

Most tricky part in this function is setting background image. The background image will be displayed when user selects the recommendation card in LeanbackLauncher. This background image is specified by “extra” field, by using Bundle. Key is Notification. EXTRA_BACKGROUND_IMAGE_URI and the value is the URI of backgroundimage. Please note that you can specify bitmap file itself for

  • smallicon – used as application/company logo, shown in the right bottom side of recommendation card.
  • largeicon – used as main image for recommendation card.

But you cannot specify the background image by bitmap. In this implementation, we cache bitmap background image by using content provider and specifying URI of this cached background image.

Storing part is as follows

public Notification build() {

        Bundle extras = new Bundle();
        File bitmapFile = getNotificationBackground(mContext, mId);

        ...

        // save bitmap into files for content provider to serve later
        try {
            bitmapFile.createNewFile();
            FileOutputStream fOut = new FileOutputStream(bitmapFile);
            mBackgroundBitmap.compress(Bitmap.CompressFormat.PNG, 85, fOut); // <- background bitmap must be created by mBackgroundUri, and not  mCardImageBitmap
            fOut.flush();
            fOut.close();
        } catch (IOException ioe) {
            Log.d(TAG, "Exception caught writing bitmap to file!", ioe);
        }

The part of getting background image URI is as follows. When Recommendation card is selected, it will refer extra fields with the key “Notification.EXTRA_BACKGROUND_IMAGE_URI“. The value is sent to Content provider’s openFile method. So the values are handled in the openFile method to extract the file path of already stored background image. 

    public Notification build() {

        Bundle extras = new Bundle();
        File bitmapFile = getNotificationBackground(mContext, mId);

        if (mBackgroundBitmap != null) {
            Log.d(TAG, "making URI for mBackgroundBitmap");
            extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
                    Uri.parse(BACKGROUND_URI_PREFIX + Integer.toString(mId)).toString());
        } else {
            Log.w(TAG, "mBackgroundBitmap is null");
        }

    ...

    }

    public static class RecommendationBackgroundContentProvider extends ContentProvider {

        ...

        @Override
        /*
         * content provider serving files that are saved locally when recommendations are built
         */
        public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
            Log.i(TAG, "openFile");
            int backgroundId = Integer.parseInt(uri.getLastPathSegment());
            File bitmapFile = getNotificationBackground(getContext(), backgroundId);
            return ParcelFileDescriptor.open(bitmapFile, ParcelFileDescriptor.MODE_READ_ONLY);
        }
    }

RecommendationFactory

RecommendationFactory class uses the RecommendationBuilder to create notification for recommending Movie items. Implementation is following.

package com.corochann.androidtvapptutorial;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.squareup.picasso.Picasso;

import java.net.URI;

public class RecommendationFactory {

    private static final String TAG = RecommendationFactory.class.getSimpleName();
    private static final int CARD_WIDTH = 500;
    private static final int CARD_HEIGHT = 500;
    private static final int BACKGROUND_WIDTH = 1920;
    private static final int BACKGROUND_HEIGHT = 1080;

    private Context mContext;
    private NotificationManager mNotificationManager;

    public RecommendationFactory(Context context) {
        mContext = context;
        if (mNotificationManager == null) {
            mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        }
    }


    public void recommend(int id, Movie movie) {
        recommend(id, movie, NotificationCompat.PRIORITY_DEFAULT);
    }

    /**
     * create a notification for recommending item of Movie class
     * @param movie
     */
    public void recommend(final int id, final Movie movie, final int priority) {
        Log.i(TAG, "recommend");
        /* Run in background thread, since bitmap loading must be done in background */
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "recommendation in progress");
                Bitmap backgroundBitmap = prepareBitmap(movie.getCardImageUrl(), BACKGROUND_WIDTH, BACKGROUND_HEIGHT);
                Bitmap cardImageBitmap = prepareBitmap(movie.getCardImageUrl(), CARD_WIDTH, CARD_HEIGHT);
                PendingIntent intent = buildPendingIntent(movie, id);

                RecommendationBuilder recommendationBuilder = new RecommendationBuilder(mContext)
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setBackground(backgroundBitmap)
                        .setId(id)
                        .setPriority(priority)
                        .setTitle(movie.getTitle())
                        .setDescription(movie.getDescription())
                        .setIntent(intent)
                        .setBitmap(cardImageBitmap)
                        .setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
                Notification recommendNotification = recommendationBuilder.build();
                mNotificationManager.notify(id, recommendNotification);
            }}).start();
    }

    /**
     * prepare bitmap from URL string
     * @param url
     * @return
     */
    public Bitmap prepareBitmap(String url, int width, int height) {
        Bitmap bitmap = null;
        try {
            URI uri = new URI(url);
            bitmap = Picasso.with(mContext)
                    .load(uri.toString())
                    .resize(width, height)
                    .get();
        } catch (Exception e) {
            Log.e(TAG, e.toString());
        }
        return bitmap;
    }

    private PendingIntent buildPendingIntent(Movie movie, int id) {
        Intent detailsIntent = new Intent(mContext, DetailsActivity.class);
        detailsIntent.putExtra(DetailsActivity.MOVIE, movie);
        detailsIntent.putExtra(DetailsActivity.NOTIFICATION_ID, id);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(mContext);
        stackBuilder.addParentStack(DetailsActivity.class);
        stackBuilder.addNextIntent(detailsIntent);
        // Ensure a unique PendingIntents, otherwise all recommendations end up with the same
        // PendingIntent
        detailsIntent.setAction(Long.toString(movie.getId()));

        return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
    }
}

recommend method is the main method, which makes the recommend Notification. Procedure is following

  1. Declare NotificationManager
    It is done in the Constructor of RecommendationFactory class.
    public RecommendationFactory(Context context) {
        mContext = context;
        if (mNotificationManager == null) {
            mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        }
    }
  1. Use your customized RecommendationBuilder class (it is custom class which usesNotificationCompat class)  to prepare recommendation.
    Brief explanation of each set function
    • Id – ID for this recommendation card.
    • Priority – set priority. high priority card is more likely to be shown in the recommendation row in LeanbackLauncher.
    • Background – set background bitmap, which will be shown when user select recommendation card.
    • Title – first row text of recommendation card.
    • Description – second row text of recommendation card.
    • Bitmap – main image of recommendation card.
    • SmallIcon – company/image icon.
    • FastLaneColor – set background color of text field in the recommendation card
    • Intent – set PendingIntent (action) to indicate that what will happen after user click this recommendation card. buildPendingIntent method is used to create intent in this example.
  2. Make Notification by building RecommendationBuilder
  3. Notify this Notification using NotificationManager.
    These are done in recommend method.
    public void recommend(final int id, final Movie movie, final int priority) {
        Log.i(TAG, "recommend");
        /* Run in background thread, since bitmap loading must be done in background */
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "recommendation in progress");
                Bitmap backgroundBitmap = prepareBitmap(movie.getCardImageUrl(), BACKGROUND_WIDTH, BACKGROUND_HEIGHT);
                Bitmap cardImageBitmap = prepareBitmap(movie.getCardImageUrl(), CARD_WIDTH, CARD_HEIGHT);
                PendingIntent intent = buildPendingIntent(movie, id);
                // 2 prepare recommendation
                RecommendationBuilder recommendationBuilder = new RecommendationBuilder(mContext)
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setBackground(backgroundBitmap)
                        .setId(id)
                        .setPriority(priority)
                        .setTitle(movie.getTitle())
                        .setDescription(movie.getDescription())
                        .setIntent(intent)
                        .setBitmap(cardImageBitmap)
                        .setFastLaneColor(mContext.getResources().getColor(R.color.fastlane_background));
                // 3 make notification
                Notification recommendNotification = recommendationBuilder.build();
                // 4 norify
                mNotificationManager.notify(id, recommendNotification);
            }}).start();
    }

That’s all. It is easy.

Build & run

recommendation
Recommendation card on LeanbackLauncher

You can send recommendation card by clicking “recommendation” button.

Source code is on github.

Running recommendation as service

Recommendation is a concept that application suggests user to navigate next action. So most of the time, recommendation may be done in the background. I will follow up about this in the remainder.

Official doc Recommending TV Content – Android developers explains how to do recommendation in the background service, and Google’s sample code is showing real source code implementation of usage of background service. In this sample source code, recommendation service will be launched after getting the intent of “BOOT_COMPLETED”, and it will execute recommendation in background every 30 min.

Leave a Comment

Your email address will not be published. Required fields are marked *