Building Android TV Apps with WebViews (Part 2)

Last week I explained of how I designed an application on the web and very easily made it available on Android TV. Of course, that post wasn’t complete. Although the app loaded and worked fine, it could certainly be optimized further for the platform. The biggest omission was recommendations. I wanted a way for users to get interesting content through a very specific mechanism. For this to happen, I had to add a little bit of Java.
Home Recommendations

Additionally, this app is available in the Play Store, and it wasn’t accepted immediately. I’ll explain some of the rejections I faced when designing it and how I managed to solve the problem, making the open source library even better.

Recommendations

To start with recommendations, I needed to add some simple logic for a repeating service. This is simple enough in Android with an AlarmReceiver:

 public void setAlarm(Context context){
        private static int PERIOD = 1000 * 60 * 30;
        // get a Calendar object with current time
        Calendar cal = Calendar.getInstance();
        Intent intent = new Intent(context, RecommendationsService.class);
        PendingIntent sender = PendingIntent.getBroadcast(context, 192837, intent, PendingIntent.FLAG_CANCEL_CURRENT);
 
        // Get the AlarmManager service
        AlarmManager am = (AlarmManager) context.getSystemService(context.ALARM_SERVICE);
        am.setInexactRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), PERIOD, sender);
    }

Now this service will run every half hour and get your recommendations. Specifically this pulls from the user’s queue, which is a combination of the most popular videos, videos you haven’t finished watching, and new videos pulled from your subscriptions. This means that I need to get the user’s information and cache it to recall at any point in the background. This led to new methods being added to receive user information that is already retrievable through authenticating.

onGetUserInfo({...}) { } //Javascript
getUserInfo(); //Java

These give you a JSON object containing a few basic details about a user, including a user id. This is stored in the app using a SettingsManager. A SettingsManager is just a wrapper for SharedPreferences.

Now I can write my method to receive alarm events:

 @Override
    public void onReceive(final Context context, Intent intent) {
        final SettingsManager sm = new SettingsManager(context);

As you can see I’m constructing the SettingsManager before making an HTTP request to my server. This has to be done in a new thread.

new Thread(new Runnable() {
            @Override
            public void run() {
                HttpRequest request =
                        HttpRequest.get("...");
                Log.d(TAG, request.url().toString());
                try {
                    JSONObject videos = new JSONObject(request.body());
                    JSONArray keys = videos.names();
                    Log.d(TAG, keys.toString());
                    Log.d(TAG, videos.keys().toString());
                    for(int i=0; i<5;i++) { if(i > keys.length() - 1)
                            continue;
                        String vid = keys.getString(i);
                        NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
                        nm.notify(i, buildNotification(videos.getJSONObject(vid)));
                        Log.d(TAG, "Loading video "+i+", "+vid);
                        Log.d(TAG, videos.getJSONObject(vid).getString("title"));
                    }
                } catch (JSONException | InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }).start();

You see that I call a method, buildNotification with a JSON object retrieved from the server. This is another custom method I created which creates a notification (recommendations are notifications).

Notification notification = new NotificationCompat.BigPictureStyle(
                new NotificationCompat.Builder(mContext)
                        .setContentTitle(video.getString("title"))
                        .setContentText(mDescription)
                        .setPriority(mPriority)
                        .setLocalOnly(true)
                        .setOngoing(true)
                        .setColor(mContext.getResources().getColor(android.R.color.holo_green_dark))
                        .setCategory(Notification.CATEGORY_RECOMMENDATION)
                        .setLargeIcon(thumbnail)
                        .setSmallIcon(R.drawable.ic_note)
                        .setContentIntent(launchApp(mContext))
                        .setExtras(null))
                .build();
 
 
        return notification;

After processing the video, I create a notification with all the pertinent information. This allows me to display notifications in the recommendations row.
Queuify Recommendations

I thought this was good and checked off the box for Android TV distribution. However, I was still missing something.

Now Playing Card

When media is playing in the background, like from Google Play Music, there’s a card displayed showing that media and giving a quick shortcut to open that app back up. When my app was rejected, they mentioned this as one of the omissions. So, I sought to add a now playing card. The biggest problem though, after I read through that page, was that I didn’t use a MediaSession. I’d have to create my own type of flow.
Now Playing Card

Hacking together a simple implementation was easy. I didn’t have to go fully into a MediaSession to display the card. Everything was placed into my MainActivity, which otherwise was the web app.

new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        final Bitmap image = Ion.with(getApplicationContext()).load(thumbnail).asBitmap().get();
                        PendingIntent intent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(getApplicationContext(), MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
 
                        Handler h = new Handler(Looper.getMainLooper()) {
                            @Override
                            public void handleMessage(Message msg) {
                                super.handleMessage(msg);
                                if(Build.VERSION.SDK_INT >= 21) {
                                    mSession = new MediaSession(getApplicationContext(), "MusicService");
                                    MediaMetadata metadata = new MediaMetadata.Builder()
                                            .putString(MediaMetadata.METADATA_KEY_TITLE, title)
                                            .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title)
                                            .putBitmap(MediaMetadata.METADATA_KEY_ART, image)
                                            .build();
                                    mSession.setMetadata(metadata);
                                    mSession.setCallback(new MediaSession.Callback() {
                                    });
                                    mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
                                    mSession.setActive(true);
                                    Log.d(TAG, "Issuing now playing notification");
                                }
                            }
                        };
                        h.sendEmptyMessage(0);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

Just as a quick note, a MediaSession requires a thumbnail for the media art. As the thumbnails live online, I needed to download the Bitmap. In this case, I used Koush’s Ion library. Downloading also meant I needed to put everything inside of a thread. Ultimately, the functionality I wanted lives in the middle.

I also wanted to make sure that the card disappeared when the app was closed. Otherwise it could stay alive indefinitely and confuse people.

@Override
    protected void onDestroy() {
        super.onDestroy();
 
        if(Build.VERSION.SDK_INT >= 21) {
            mSession.setActive(false);
            Log.d(TAG, "Playback ended, removing now playing notification");
        }
    }

Great. I know how to create and remove this card. Now how do I interface with my JavaScript app? This required a bit more infrastructure and platform-specific code while maintaining the compatibility with any web browser. Additionally, this was a very specific feature that really didn’t deserve code in the library devoted to it. So I came up with an entirely different way to run custom actions on Android.

if(ANDROID) {
    Android.runCustomCode('{action:"NOW_PLAYING", title:"'+nowplaying.title+'", thumbnail:"'+nowplaying.thumbnail+'"}');
}

This is a JavaScript item that is running in the browser. You could already use the ANDROID boolean to determine if this was being run in an Android app, but now it’s being checked and if true executes this new method.

Android.runCustomCode isn’t as scary as it might first appear. What it allows you to do is bridge the gap between Android and web by supplying JSON objects to the app and adding an interpreter for that JSON. This allows you to send any data to Android. On your MainActivity you can add a method that can use that parsed JSON.

@Override
    public void onCustomAction(JSONObject js) throws JSONException {
        super.onCustomAction(js);
        String action = js.getString("action");
        final int notification_id = 200;
        if(action.equals("NOW_PLAYING")) {
            //Issue a now playing notification
            final String title = js.getString("title");
            final String thumbnail = js.getString("thumbnail");

As you see, I can override the onCustomAction method and check the action. If the action is the same, I can get the title and thumbnail of the current video. Do you remember that thread from earlier that created the now playing card? Well now I have the data needed to populate it. The library remains very small and simple, yet very extendable.

So I can add a second set of instructions that remove the card when the video ends.

if(ANDROID) {
    console.log('{action:"REMOVE_NOW_PLAYING"}');
    Android.runCustomCode('{action:"REMOVE_NOW_PLAYING"}');
}
else if(action.equals("REMOVE_NOW_PLAYING")) {
            //Remove now playing notification
            if(Build.VERSION.SDK_INT >= 21) {
                mSession.setActive(false);
                Log.d(TAG, "Playback ended, removing now playing notification");
            }
        }

So I submitted the app again, assuming it would be approved this time. When it was rejected, I saw my library still had a few flaws.

Key Input

One of the useful things about the library was it abstracted away platform differences. I could press a gamepad’s A or Enter and they’d be interpreted the same and read the same. As it turns out though, Android has an expansive key library. Many keys seem to do the same thing, so I need to make several aliases for simple input development.

case KeyEvent.KEYCODE_BUTTON_A:
    eval("GamePad.pressKey(GamePad.KEYS.Enter)");
    return true;
case KeyEvent.KEYCODE_ENTER:
    eval("GamePad.pressKey(GamePad.KEYS.Enter)");
    return true;

Additionally I had to accommodate some of Android’s unique keys.

case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
    eval("GamePad.pressKey(GamePad.KEYS.Play_Pause)");
    return true;

I added stepping to the DPAD, so you could step through each video instead of pressing down and flying through six before you know it. It’s easy to disable though.

boolean scrollStarted;.
boolean ENABLE_QUICKSCROLL = false;
switch (press) {
    case Dpad.LEFT:
        // Do something for LEFT direction press
        if(ENABLE_QUICKSCROLL || !scrollStarted) {
            eval("GamePad.pressKey(GamePad.KEYS.Left)");
        }
        scrollStarted = true;
        return true;
}

With those changes, my app was finally approved for distribution in the Google Play Store for Android TV. It took some time and finagling to get it to work, but it did. Now you can take use the same open source library I did to port your own web app to Android TV. All the hard work is done. Why not?

Nick Felker

Nick Felker

Nick Felker is a student Electrical & Computer Engineering student at Rowan University (C/O 2017) and the student IEEE webmaster. When he's not studying, he is a software developer for the web and Android (Felker Tech). He has several open source projects on GitHub (http://github.com/fleker)Devices: Moto G-2013 Moto G-2015, Moto 360, Google ADT-1, Nexus 7-2013 (x2), Lenovo Laptop, Custom Desktop.Although he was an intern at Google, the content of this blog is entirely independent and his own thoughts.

More Posts - Website

Follow Me:
TwitterLinkedInGoogle PlusReddit