Developing for the Chromecast, Part 4: The HipstaCaster Android app

Developing for the Chromecast, Part 4: The HipstaCaster Android app
november 3, 2013 erik.lupander@squeed.com
Screenshot_2013-10-23-22-28-59

The photo GridView. The photos shown are loaded off Flickr, the visible ones by user ‘distantranges’. I warmly recommend you check out his/hers amazing photos on Flickr.com yourselves!

The final piece of this example Chromecast application stack is the Android Sender application shown on the right.

Again, nothing really fancy as these examples were developed to try out the Chromecast programming model and not create some revolutionary UX application for photo viewing. Again, the app is loosely based on the ubiquitous Tic-tac-toe example from Google.

General Overview
Plain and simple, it’s a GridView with an ActionBar on top. The four buttons do the following:

  • Wrench: Preferences page, change tag(s) to search for.
  • Cast icon: Click to select device to cast to. Note that this icon is not shown if the MediaRouter can’t find a valid route to cast to on the network.
  • Refresh: Empties the GridView and reloads images over the REST service according to the tag(s) set in the Preferences page.
  • Play: Starts a slideshow on the TV from the first, or latest selected image.

In addition to this, with an active Cast Session (blue cast icon), tapping on any given image will show it on the TV. There is also a somewhat crude on-demand loading of more images every time the GridView has scrolled to the bottom. A loading indicator will be shown on the bottom of the page. Please note that loading of more images into the view is a two-step process. First, we query our own REST API using the /rest/search/{tags}?…. endpoint to get image metadata, then the thumbnail images are concurrently loaded from flickr using the thumbnail URLs from the metadata. Of course, all network IO happens on background threads. Please note this blog post isn’t about top-notch Android coding practices, I’m very sure there’s better and cleaner ways to accomplish a lot of these things…

Getting the MediaRouter set up and connecting to the Chromecast Device
First off, let’s take a look at the Activity for the HipstaCaster application.

public class HipstaActivity extends ActionBarActivity implements MediaRouteAdapter {

The ActionBarActivity is just a subclassing so we get the Actionbar. The onDeviceAvailable method we’ll implement as specified by the MediaRouteAdapter interface will later on provide the plumbing necessary to pass the selected Media Route to the activity.

We also have some code that sets up the Cast context and makes things possible.

private void initMediaRouter() {
	mSessionListener = new SessionListener();
        mMessageStream = new CustomHipstaCasterStream(); // (1)

        mCastContext = new CastContext(getApplicationContext());
        MediaRouteHelper.registerMinimalMediaRouteProvider(mCastContext, this);

        mMediaRouter = MediaRouter.getInstance(getApplicationContext());
        mMediaRouteSelector = MediaRouteHelper.buildMediaRouteSelector(
                MediaRouteHelper.CATEGORY_CAST, APP_NAME, null); // (2)
        mMediaRouterCallback = new MediaRouterCallback(); // (3)
}

So, a bunch of inits. Is that all there is? Well, sort of. The code above typically runs in the scope of the standard Activity onCreate() method and should be pretty standard for discovering Cast devices from Android apps with some // (n) comments thrown in for the more interesting things:

// (1): Here we create an instance of our custom MessageStream, providing a declarative interface for sending messages over to the cast device, as well as callbacks for receiving stuff back.

// (2): This MediaRoute selector is a tricky one. See that APP_NAME parameter? That’sScreenshot_2013-10-23-22-29-03 your app’s API key. And unless your API key is whitelisted and the Chromecast gets a URL back from whatever service Google’s using to map API keys to URL:s, that call to build a MediaRouteSelector with an API key as second parameter will … hide the cast button in the ActionBar. Yes, the Chromecast device must recognize the API key to be eligible as media route. I spent quite some time cursing over the disappearing cast icon before my whitelisting request had been approved by Google. There is a workaround though, just supply null values for API key and protocol and all cast devices will become visible to your application. You won’t be able to cast to them, but at least you can play around with the discovery mechanisms.

// (3): This class provides some important callbacks for when a route has been selected, e.g. the user selects a Chromecast device from the list displayed in the screenshot on the right. In these callbacks, important stuff like initializing the ApplicationSession, attaching the MessageStream and registering a SessionListener implementation to the ApplicationSession happens. Also, once a user deselects (e.g. disconnects) from their cast device, this is the place to clean up and make your app behave in a sensible way.

After onCreate() has finished running, we need to tell the MediaRouter how to actually scan for devices. This is done in the onStart() callback.

@Override
protected void onStart() {
    super.onStart();
    mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
            MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
}

The MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN tells the MediaRouter to start scanning. I think it’s a somewhat strange way to make the MediaRouter start its scanning. Anyway, you can also see here how the MediaRouter.Callback we defined earlier is attached to the Router, so when a MediaRouter event occurs the code below can execute.

private class MediaRouterCallback extends MediaRouter.Callback {         

        @Override
        public void onRouteSelected(MediaRouter router, android.support.v7.media.MediaRouter.RouteInfo route) {
            Log.i(TAG, "onRouteSelected: " + route);
            HipstaActivity.this.onRouteSelected(route);
        }

        @Override
        public void onRouteUnselected(MediaRouter router, android.support.v7.media.MediaRouter.RouteInfo route) {
            Log.i(TAG, "onRouteUnselected: " + route);
            HipstaActivity.this.onRouteUnselected(route);
        }
}

And in the next step, the onRouteSelected callback will just execute this piece of code:

private void onRouteSelected(android.support.v7.media.MediaRouter.RouteInfo route) {
    sLog.d("onRouteSelected: %s", route.getName());
    MediaRouteHelper.requestCastDeviceForRoute(route);
}

So, that MediaRouteHelper.requestCastDeviceForRoute method call will finally trigger the onDeviceAvailable method we’ve overridden as per the MediaRouteAdapter interface our Activity is implementing.

    @Override
    public void onDeviceAvailable(CastDevice device, String routeId,
                                  MediaRouteStateChangeListener listener) {
        sLog.d("onDeviceAvailable: %s (route %s)", device, routeId);
        setSelectedDevice(device);
    }

And finally, we can hook up the final bits and pieces in the setSelectedDevice method:

private void setSelectedDevice(CastDevice device) {            
        mSelectedDevice = device;
        if (mSelectedDevice != null) {            
            mSession = new ApplicationSession(mCastContext, mSelectedDevice);
            mSession.setListener(mSessionListener);

            try {
                mSession.startSession(APP_NAME);
            } catch (IOException e) {
                Log.e(TAG, "Failed to open a session", e);
            }
        } else {
            endSession();
        }
    }

Most notable, the call to mSession.startSession(APP_NAME) will contact the Chromecast device and trigger the API key lookup, do the loading of the receiver app from the whitelisted URL and so on.

So, there’s a chain of initalizations and callbacks needed to make it all come together. As always, feel free to browse the full source code of the HisptaActivity to see the code for this.

That ActionBar has some quirks to make it work on older versions of Android. The ActionBar class, providing a somewhat iOS-like top bar with menu items, overflowing item handling etc is a pretty neat thing introduced in Android 3.0 Honeycomb. But it’s possible to use back to Android 2.2 through the use of the AppCompat support library. Anyway, getting it all set up properly was quite a mess for me as explained in the second installment of this blog series. Nevertheless, the actual MenuItem for the Chromecast button is added like this:

MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
MediaRouteActionProvider mediaRouteActionProvider =
        (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem);
mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);

where the XML for the menu item looks like this

<item
   android:id="@+id/media_route_menu_item"
   android:title="@string/media_route_menu_title"  
   app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
   app:showAsAction="always"/>

So there’s the ”app” namespace and a special ”actionProviderClass” attached to the MenuItem that ties the MediaRouter stuff to our ActionBar. I would perhaps label this way of providing the cast functionality in Android as a bit exaggerated for this particular example, but do note that the official chromecast design guidelines explicitly states the the cast icon shall be provided in an ActionBar and if possible always be the rightmost icon. Read these guidelines yourself, they’re pretty comprehensive and a good guide.

The GridView, GridViewAdapter and the loading of thumbnails
To be honest, this stuff is not really Chromecast-related, so I’ll almost skip right past it. I just want to mention that there are some seriously kludgy flags and index counters in the code to work around some issues with the GridView and when there’s more photo metadata loaded from our REST backend than there have been sub views added to the grid. Simply put, the code makes sure that the full search for photo metadata over our REST backend and the actual loading of each of the 20 thumbnails finish before allowing the next batch of 20 to load (in case the user likes to scroll to the bottom very fast and very often). Also, the GridViewAdapter does some rather ugly things to make the listener that sends the ‘showPhoto’ message over to the Chromecast to work:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = convertView;
    ViewHolder holder = null;        

    if (row == null) {
       LayoutInflater inflater = ((Activity) context).getLayoutInflater();
       row = inflater.inflate(layoutResourceId, parent, false);
       row.setOnClickListener(new GridViewOnClickListener());
       // more code below of course
private class GridViewOnClickListener implements OnClickListener {

	@Override
	public void onClick(View v) {    	
		ViewHolder tag = (ViewHolder) v.getTag();
		((HipstaActivity) context).setOffset(tag.position);
		((HipstaActivity) context).openPhoto(tag.title, tag.fullsizeUrl, tag.ownerName, tag.description);					
	}
    }

I’m sure this solution can compete in the Android anti-pattern World Championship, basically I’m misusing the ViewHolder to hold a bit more than the text and image views for each thumbnail. I really should make something more neat. There are mainly two reasons for this: First, why are the click listener declared and assigned inside the GridViewAdapter? We’ll, first off, getting the callback set on a gridView.setOnItemClickListener to fire turned out to be mission impossible. At least with the limited amount of time I had to spend on such non-chromecast things writing these blog posts. Please feel free to enlighten me. Second, since the GridViewAdapter recycles views so you can’t declare the position integer as final and refer to it in the callback (if declared inline). You’ll end up with only the top 10 or so photo URLs being sent over to the Chromecast when you tap the image due to the view recycling. Trust me, I did.

Sending and receiving messages
To be honest, this is pretty much what all of this is about. The ability to easily do bi-directional messaging with minimal latency between sender and receiver is what makes the Chromecast model very interesting for many use-cases not directly related to consuming the latest offerings on Netflix and YouTube. And from our programmer’s perspective, this is amazingly easy. In the subclass of the MessageStream, a sample ”send” method may look like this:

public final void openPhotoOnChromecast(String title, String url, String ownerName, String description) {
    try {
        Log.d(TAG, "openPhotoOnChromecast: " + url);
        JSONObject payload = new JSONObject();
        payload.put(KEY_COMMAND, "viewphoto");
        payload.put("fullsizeUrl", url);
        payload.put("ownerName", ownerName);
        payload.put("title", title);
        payload.put("description", description);

        sendMessage(payload); // yes, this is it!
    } catch (JSONException e) {
        Log.e(TAG, "Cannot parse or serialize data for openPhotoOnChromecast", e);
    } catch (IOException e) {
        Log.e(TAG, "Unable to send openPhotoOnChromecast message", e);
    } catch (IllegalStateException e) {
        Log.e(TAG, "Message Stream is not attached", e);
    }
}

Considering most of the code above is about assembling a JSONObject and catching errors, the sendMessage method is very very simple. Note that the sendMessage method is declared protected, but not final, which means you must subclass the MessageStream to do anything custom with it. Well, you need to subclass MessageStream anyway since it’s abstract. As I mentioned earlier, there is a default MessageStream implementation provided out of the box called MediaProtocolMessageStream. I havn’t played around with it myself, but I think it’s supposed to fit very nicely with the HTML5 MediaController.

Anyway, receiving a message is just as simple.

@Override
public void onMessageReceived(JSONObject message) {
    try {
	Log.d(TAG, "onMessageReceived: " + message);
	// Do something with the message payload, 
        // switch(message.respType) or similar maybe?
	} catch (JSONException e) {
		Log.w(TAG, "Message doesn't contain an expected key.", e);
	}
}

In my HipstaCaster application, I have a subclass of the ”main” HipstaCasterMessageStream class declared as a private class inside the HipstaActivity. This is so I lazily can do UI updates directly on views declared in this activity when the receiver sends something to me, such as informing me the slideshow has moved on to the next picture.

@Override
protected void onCurrentSlideShowImageMessage(String message) {
	mInfoView.setText(message);
}

Final words
So, it’s finally time to wrap up this series of blog posts. I think the Chromecast as a device has a bright future ahead of it, especially when Google finalizes the Google Cast SDK and starts to globally whitelist 3rd party applications. Since the sender applications isn’t Android and/or iOS exclusive I do think Chromecast support will become as commonplace as AirPlay support is for many iOS apps – perhaps with the difference that Google’s traditional openness and the inherent programmability of the Chromecast stack may lead to a more diverse adaptation across application domains.

I hope these blog posts and the Hipstacaster example may have been helpful to anyone interested in ”how stuff works”. I do recommend diving into the Google Cast Home Page, the API reference and developer’s guide there. Most of what I’ve written is after all based on the information there and their example applications.

Feel free to leave a comment or ask some question. The full source code for the REST application, the Android sender app and the receiver app is available on my GitHub page.

1 Kommentar

Pingbacks

  1. […] have found out method (on blog) of how possible send pictures local storage. , yeah, doesn’t seem […]

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *

*

Denna webbplats använder Akismet för att minska skräppost. Lär dig hur din kommentardata bearbetas.