Monday, July 14, 2014

How to integrate your Android app with the CodeRed launcher

What is CodeRed?

CodeRed is an ambitious new Android TV box launcher with a strong emphasis on simplicity, beauty and flexability.  One of the really cool features of the CodeRed launcher is it's plugin architecture for it's Film strip control.  The launcher comes packaged with several Film strip plugins that provide content for the film strip, such as Youtube, Facebook, Vimeo, Instagram and Vine.  But it doesn't stop there.  With the CodeRed API it's quick and easy for anyone to integrate and provide content for the CodeRed film strip.

NOTE: Since the CodeRed launcher hasn't been released yet, if you'd like a test build to play with and use to test your apps integration with it, please email me at robwoods.edbrock@gmail.com and I'll send you the latest build and provide any support you need to get your plugin working.  Also, this isn't the final code for the API, there won't be any major changes, but there will be a few small changes I'll need to make before the final release such as a versioning system.

Overview

In what scenarios would you want to provide content to the CodeRed launcher?  Here are a couple of possibilities..

1. You maintain an app that provides your users with video content from the internet and would like this content to be easily accessible on a TV using an Android TV box.  When a users selects your video on the CodeRed launcher, it starts streaming immediately without even running your app.
2. You maintain an app that contains it's own local content that you'd like presented to the user on a TV directly from the launcher.  When a user selects your content in the launcher, it launches that content directly in your app.
3. Some combination of the above 2 scenarios.  For example, perhaps the content you are exposing to the CodeRed launcher requires further processing before streaming.  You could provide presentation content to the launcher and when the user selects your content, your app is invoked and any further processing can take place before playing the content.

Before I go into detail explaining how you can integrate with the CodeRed launcher, I will describe what is involved behind the scenes.  Before I get into it, I'd like to thank Roman Nurik for sharing his very clever pattern for publishing and subscribing to content between 2 apps using IntentService.  We are using a very similar pattern in the CodeRed launcher for allowing others to publish content to the launcher.  You can see Roman's awesome live wallpaper app on GitHub here.

The concept is actually very simple.  The CodeRed launcher has implemented a subscriber IntentService that handles incoming requests from apps that want to publish content to the launcher.  In turn, an app that wants to publish content is doing so by subclassing an IntentService based class (from the CodeRed API) and implementing the onTryUpdate() method.  I'll go into more detail to help with the specific implementation later, but that is the basic  flow.

Lets get started


  1. You'll need the CodeRed API library first.  You can get it here.
  2. Copy this library to your apps libs folder or link the CodeRed API library project to your project in Eclipse.  Now you should have access to the CodeRed API in your app.
  3. Create a new subclass of RemoteCodeRedFilmSource.
  4. Override and implement the onTryUpdate(int reason) method.  This is the only method that you need to implement.  In this method you will need to prepare your content and create a list of FilmItem's.  FilmItem is a data object that is part of the CodeRed API.  Inside the FilmItems are various data members that describe your content, including title, description, thumbnail Uri, content Uri and an Intent.  There's also support for some branding elements, but I'll talk about them in another blog post.  Most of the elements of a FilmItem are pretty self explanatory, however, the Intent is an interesting one.  In order for a user to actually do something with your content in the launcher, you will either need to supply a content Uri or an Intent.  If you supply a content Uri, when the user selects it, the launcher will launch an Intent with ACTION_VIEW and use the Uri you supplied.  If this is a video stream, then a video player should handle it, if it's an http link then the web browser should handle it, etc.

    If you supplied an Intent, then instead of the launcher letting the OS decide what to do with your content, you can specify exactly what you want to happen by supplying a specific Intent.  So, for example if you want your app to be invoked specifically when a user selects your content, you should provide an Intent that launches your app specifically with whatever extra data is required.

    Don't forget to call scheduleUpdate(long timeInMillis) as the last thing you do.  This ensures that your plugin will continue to publish new data to the launcher at the interval you specify.
  5. There is one last step that is required.  You need to update your apps Manifest file with some information that will allow the CodeRed launcher to know that you have implemented a plugin with the CodeRed API.  All you need to do is add this specific action to your IntentServices definition in your Manifest.

    <intent-filter>
         <action android:name="com.brockoli.android.codered.api.CodeRedFilmSource" />
    </intent-filter>

Here is a complete example showing how to provide trending Youtube videos to the CodeRed launcher.

public class YoutubeFilmSource extends RemoteCodeRedFilmSource {
    private static final String TAG = LogUtil.makeLogTag(YoutubeFilmSource.class);
    private static final String SOURCE_NAME = "Youtube videos";

public YoutubeFilmSource() {
super(SOURCE_NAME);
}

@Override
protected void onTryUpdate(int reason) throws RetryException {
        List<FilmItem> currentFilms = getCurrentFilms();
        JSONObject jsonObject;
        ArrayList<FilmItem> films = new ArrayList<FilmItem>();
     
        try {
            jsonObject = IOUtil.fetchJsonObject(getString(R.string.youtube_videos_url));
JSONObject jsonFeed = jsonObject.getJSONObject("feed");
JSONArray entries = jsonFeed.getJSONArray("entry");
for (int i=0; i < ((entries.length() > 20) ? 20 : entries.length()); i++) {
JSONObject entry = entries.getJSONObject(i);
JSONObject mediaGroup = entry.getJSONObject("media$group");
JSONArray thumbnails = mediaGroup.getJSONArray("media$thumbnail");
JSONObject description = mediaGroup.getJSONObject("media$description");
JSONArray links = entry.getJSONArray("link");
JSONObject title = entry.getJSONObject("title");

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(links.getJSONObject(0).getString("href")));
intent.putExtra("force_fullscreen",true);

FilmItem film = new FilmItem.Builder()
.title(title.getString("$t"))
.byline(description.getString("$t"))
.thumbUri(Uri.parse(thumbnails.getJSONObject(0).getString("url")))
.contentUri(Uri.parse(links.getJSONObject(0).getString("href")))
.viewIntent(intent)
.build();

films.add(film);
}
        } catch (JSONException e) {
            LOGE(TAG, "Error reading JSON", e);
            throw new RetryException(e);
        } catch (IOException e) {
            LOGE(TAG, "Error reading JSON", e);
            throw new RetryException(e);
        }

        if (films != null && currentFilms != null && films.equals(currentFilms)) {
            LOGD(TAG, "Skipping update of same content.");
        } else {
            LOGD(TAG, "Publishing content update: " + films);
            if (films != null && jsonObject != null) {
            LOGD(TAG,"Youtube film source publishing films to launcher");
                publishFilms(films);
            }
        }
        // show the latest photo in 30 minutes
        scheduleUpdate(System.currentTimeMillis() + 30 * 60 * 1000);
    }
}