-
Notifications
You must be signed in to change notification settings - Fork 13
Android Sender App Development
This overview shows how to build MatchStick sender applications for Android using the MatchStick SDK. In this overview, sender application or MatchStick application refers to an app running on a mobile device (the sender device) and receiver application refers to an HTML5 application running on Flint dongle.
Since the libraries contribute resources, you cannot simply satisfy the dependencies by including their JAR files; instead you need to import them as library projects for your IDE.
- Git Clone the Flint Android Sender SDK project
- Add the flint-android-sender-sdk, v7-appcompat and v7-mediarouter library to your project. For setup instructions, read Android Support Library Setup and be sure you follow the section about Adding libraries with resources.
The following libraries are required as dependencies for your app:
- android-support-v7-appcompat which can be found at < MatchStick SDK location>/lib_source/appcompat.
- android-support-v7-mediarouter which can be found at < MatchStick SDK location>/lib_source/mediarouter (this has a dependency on android-support-v7-appcompat).
- flint_sdk_android which can be found at < MatchStick SDK install location>.
Your AndroidManifest.xml file requires the following configuration to use the MatchStick SDK.
The minimum Android SDK version that the MatchStick SDK supports is 9 (GingerBread).
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="19" />
Sender application need at least below 3 permissions to work with MatchStick receiver device.
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
MatchStick sender application require a service defined in AndroidManifest.xml:
<service
android:name="tv.matchstick.flint.service.FlintDeviceService"
android:exported="false">
<intent-filter>
<action android:name="android.media.MediaRouteProviderService" />
</intent-filter>
</service>
The application’s theme needs to be correctly set based on the minimum Android SDK version. For example, you may need to use a variant of Theme.AppCompat.
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat" >
...
</application>
For a comprehensive listing of all classes, methods and events in the MatchStick Android SDK, see the MatchStick Android API Reference. The following sections cover the details of the typical execution flow for a sender application; here is a high-level list of the steps:
- Sender app starts MediaRouter device discovery: MediaRouter.addCallback
- MediaRouter informs sender app of the route the user selected: MediaRouter.Callback.onRouteSelected
- Sender app retrieves FlintDevice instance: FlintDevice.getFromBundle
- Sender app creates a FlintManager: FlintManager.Builder
- Sender app connects the FlintManager: FlintManager.connect
- SDK confirms that FlintManager is connected: FlintManager.ConnectionCallbacks.onConnected
- Sender app launches the receiver app: Flint.FlintApi.launchApplication
- SDK confirms that the receiver app is connected: ResultCallback<Flint.ApplicationConnectionResult>
- Sender app creates a communication channel: Flint.FlintApi.setMessageReceivedCallbacks
- Sender sends a message to the receiver over the communication channel: Flint.FlintApi.sendMessage
Flint device discovery may be performed using the Android MediaRouter APIs in the Android Support Library, with compatibility back to Android 2.1. For more information about the Android MediaRouter, see the MediaRouter Developer Guide. The MediaRouter framework provides a Flint button and a list selection dialog for selecting a route. The MediaRouter framework interfaces with the MatchStick SDK via a MediaRouteProvider implementation to perform the discovery on behalf of the application. There are three ways to support a Flint button:
- Using the MediaRouter ActionBar provider: android.support.v7.app.MediaRouteActionProvider
- Using the MediaRouter Flint button: android.support.v7.app.MediaRouteButton
- Developing a custom UI with the MediaRouter API’s and MediaRouter.Callback
This document describes the use of the MediaRouteActionProvider to add the Flint button to the ActionBar. The MediaRouter ActionBar provider needs to be added to the application’s menu hierarchy defined in XML:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<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"/>
...
</menu>
The application Activity needs to extend ActionBarActivity:
public class MainActivity extends ActionBarActivity {
...
}
The application needs to obtain an instance of the MediaRouter and needs to hold onto that instance for the lifetime of the sender application:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mMediaRouter = MediaRouter.getInstance(getApplicationContext());
}
We need set Application's ID first. The Id can be one of them:
- Start with "~": which means this application will use some Flint internal specific features.
- Not start with "~": Standard DIAL application ID.
Flint.setApplicationId("YOUR_APPLICATION_ID");
The MediaRouter needs to filter discovery for Flint devices that can launch the receiver application associated with the sender app. For that a MediaRouteSelector is created by calling MediaRouteSelector.Builder:
mMediaRouteSelector = new MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent.categoryForCast("YOUR_APPLICATION_ID"))
.build();
The MediaRouteSelector is then assigned to the MediaRouteActionProvider in the ActionBar menu:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.main, menu);
MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
MediaRouteActionProvider mediaRouteActionProvider = (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem);
mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
return true;
}
Now MediaRouter will use the selector to filter the devices that are displayed to the user when the Flint button in the ActionBar is pressed.
When the user selects a device from the Flint button device list, the application is informed of the selected device by extending MediaRouter.Callback:
private class MyMediaRouterCallback extends MediaRouter.Callback {
@Override
public void onRouteSelected(MediaRouter router, RouteInfo info) {
mSelectedDevice = FlintDevice.getFromBundle(info.getExtras());
String routeId = info.getId();
...
}
@Override
public void onRouteUnselected(MediaRouter router, RouteInfo info) {
teardown();
mSelectedDevice = null;
}
}
The application needs to trigger the discovery of devices by adding the MediaRouter.Callback to the MediaRouter instance. Typically this callback is assigned when the application Activity is active and then removed when the Activity goes into the background, as shown below.
@Override
protected void onResume() {
super.onResume();
mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
@Override
protected void onPause() {
if (isFinishing()) {
mMediaRouter.removeCallback(mMediaRouterCallback);
}
super.onPause();
}
You should also add the callback during onStart() and remove it during onStop().
@Override
protected void onStart() {
super.onStart();
mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
@Override
protected void onStop() {
mMediaRouter.removeCallback(mMediaRouterCallback);
super.onStop();
}
Once the application knows which Flint device the user selected, the sender application can launch the receiver application on that device. The MatchStick SDK API’s are invoked using FlintManager. A FlintManager instance is created using the FlintManager.Builder and requires various callbacks that are discussed later in this document:
Flint.FlintOptions.Builder apiOptionsBuilder = Flint.FlintOptions.builder(mSelectedDevice, mFlintListener);
mApiClient = new FlintManager.Builder(this)
.addApi(Flint.API, apiOptionsBuilder.build())
.addConnectionCallbacks(mConnectionCallbacks)
.build();
The application can then establish a connection using the FlintManager instance:
mApiClient.connect();
The application needs to declare FlintManager.ConnectionCallbacks to be informed of the connection status. All of the callbacks run on the main UI thread. Once the connection is confirmed, the application can launch the application by specifying the url:
private class ConnectionCallbacks implements FlintManager.ConnectionCallbacks {
@Override
public void onConnected(Bundle connectionHint) {
if (mWaitingForReconnect) {
mWaitingForReconnect = false;
reconnectChannels();
} else {
try {
Flint. FlintApi.launchApplication(mApiClient, "YOUR_APPLICATION_URL", false).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {
@Override
public void onResult(Flint.ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
String applicationStatus = result.getApplicationStatus();
boolean wasLaunched = result.getWasLaunched();
...
} else {
teardown();
}
}
});
} catch (Exception e) {
Log.e(TAG, "Failed to launch application", e);
}
}
}
@Override
public void onConnectionSuspended(int cause) {
mWaitingForReconnect = true;
}
@Override
public void onConnectionFailed(ConnectionResult result) {
teardown();
}
}
If FlintManager.ConnectionCallbacks.onConnectionSuspended is invoked when the client is temporarily in a disconnected state, your application needs to track the state, so that if FlintManager.ConnectionCallbacks.onConnected is subsequently invoked when the connection is established again, the application should be able to distinguish this from the initial connected state. It is important to re-create any channels when the connection is re-established.
The Flint.Listener callbacks are used to inform the sender application about receiver application events:
mFlintListener = new Flint.Listener() {
@Override
public void onApplicationStatusChanged() {
if (mApiClient != null) {
Log.d(TAG, "onApplicationStatusChanged: " + Flint.FlintApi.getApplicationStatus(mApiClient));
}
}
@Override
public void onVolumeChanged() {
if (mApiClient != null) {
Log.d(TAG, "onVolumeChanged: " + Flint.FlintApi.getVolume(mApiClient));
}
}
@Override
public void onApplicationDisconnected(int errorCode) {
teardown();
}
};
For the sender application to communicate with the receiver application, a custom channel needs to be created. The sender can use the custom channel to send String messages to the receiver. Each custom channel is defined by a unique namespace and must start with the prefix urn:flint:, for example, urn:flint:com.example.custom. It is possible to have multiple custom channels, each with a unique namespace.
The custom channel is implemented with the Flint.MessageReceivedCallback interface:
class HelloWorldChannel implements Flint.MessageReceivedCallback {
public String getNamespace() {
return " urn:flint:com.example.custom";
}
@Override
public void onMessageReceived(FlintDevice flintDevice, String namespace, String message) {
Log.d(TAG, "onMessageReceived: " + message);
}
}
Once the sender application is connected to the receiver application, the custom channel can be created using Flint.FlintApi.setMessageReceivedCallbacks:
Flint.FlintApi.launchApplication(mApiClient, "YOUR_APPLICATION_URL", false).setResultCallback(new ResultCallback<Flint.ApplicationConnectionResult>() {
@Override
public void onResult(Flint.ApplicationConnectionResult result) {
Status status = result.getStatus();
if (status.isSuccess()) {
ApplicationMetadata applicationMetadata = result.getApplicationMetadata();
String applicationStatus = result.getApplicationStatus();
boolean wasLaunched = result.getWasLaunched();
mApplicationStarted = true;
mHelloWorldChannel = new HelloWorldChannel();
try {
Flint.FlintApi.setMessageReceivedCallbacks(mApiClient, mHelloWorldChannel.getNamespace(), mHelloWorldChannel);
} catch (IOException e) {
Log.e(TAG, "Exception while creating channel", e);
}
}
}
});
Once the custom channel is created, the sender can use that to send String messages to the receiver over that channel:
private void sendMessage(String message) {
if (mApiClient != null && mHelloWorldChannel != null) {
try {
Flint.FlintApi.sendMessage(mApiClient, mHelloWorldChannel.getNamespace(), message).setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (!result.isSuccess()) {
Log.e(TAG, "Sending message failed");
}
}
});
} catch (Exception e) {
Log.e(TAG, "Exception while sending message", e);
}
}
}
The application can encode JSON messages into a String, if needed, and then decode the JSON String in the receiver.
The MatchStick SDK supports a media channel to play media on a receiver application. The media channel has a well-known namespace of urn:flint:org.openflint.fling.media. To use the media channel create an instance of RemoteMediaPlayer and set the update listeners to receive media status updates:
mRemoteMediaPlayer = new RemoteMediaPlayer();
mRemoteMediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() {
@Override
public void onStatusUpdated() {
MediaStatus mediaStatus = mRemoteMediaPlayer.getMediaStatus();
boolean isPlaying = mediaStatus.getPlayerState() == MediaStatus.PLAYER_STATE_PLAYING;
...
}
});
mRemoteMediaPlayer.setOnMetadataUpdatedListener(new RemoteMediaPlayer.OnMetadataUpdatedListener() {
@Override
public void onMetadataUpdated() {
MediaInfo mediaInfo = mRemoteMediaPlayer.getMediaInfo();
MediaMetadata metadata = mediaInfo.getMetadata();
...
}
});
Once the sender application is connected to the receiver application, the media channel can be created using Flint.FlintApi.setMessageReceivedCallbacks:
try {
Flint.FlintApi.setMessageReceivedCallbacks(mApiClient, mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer);
} catch (IOException e) {
Log.e(TAG, "Exception while creating media channel", e);
}
mRemoteMediaPlayer
.requestStatus(mApiClient)
.setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (!result.getStatus().isSuccess()) {
Log.e(TAG, "Failed to request status.");
}
}
});
You need to call RemoteMediaPlayer.requestStatus() and wait for the OnStatusUpdatedListener callback. This will update the internal state of the RemoteMediaPlayer object with the current state of the receiver.
To load media, the sender application needs to create a MediaInfo instance using MediaInfo.Builder. The MediaInfo instance is then used to load the media with the RemoteMediaPlayer instance:
MediaMetadata mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, "My video");
MediaInfo mediaInfo = new MediaInfo.Builder(
"http://your.server.com/video.mp4")
.setContentType("video/mp4")
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.build();
try {
mRemoteMediaPlayer.load(mApiClient, mediaInfo, true).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
Log.d(TAG, "Media loaded successfully");
}
}
});
} catch (IllegalStateException e) {
Log.e(TAG, "Problem occurred with media during loading", e);
} catch (Exception e) {
Log.e(TAG, "Problem opening media during loading", e);
}
Once the media is playing, the sender application can control the media playback using the RemoteMediaPlayer instance:
mRemoteMediaPlayer.pause(mApiClient).setResultCallback(new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult result) {
Status status = result.getStatus();
if (!status.isSuccess()) {
Log.w(TAG, "Unable to toggle pause: " + status.getStatusCode());
}
}
});
It is very important for sender applications to handle all error callbacks and decide the best response for each stage of the Flint life cycle. The application can display error dialogs to the user or it can decide to tear down the connection to the receiver. Tearing down the connection has to be done in a particular sequence:
private void teardown() {
Log.d(TAG, "teardown");
if (mApiClient != null) {
if (mApplicationStarted) {
if (mApiClient.isConnected() || mApiClient.isConnecting()) {
try {
Flint.FlintApi.stopApplication(mApiClient);
if (mHelloWorldChannel != null) {
Flint.FlintApi.removeMessageReceivedCallbacks(mApiClient, mHelloWorldChannel.getNamespace());
mHelloWorldChannel = null;
}
} catch (IOException e) {
Log.e(TAG, "Exception while removing channel", e);
}
mApiClient.disconnect();
}
mApplicationStarted = false;
}
mApiClient = null;
}
mSelectedDevice = null;
mWaitingForReconnect = false;
}
Several Matchstick sample sender apps have been open sourced on GitHub.
- Flint Videos Sample Sender for Android
Contact: [email protected]
Google Groups: https://groups.google.com/forum/#!forum/openflint
-
Flint
- [Developer's Guide](Developer Guide for Flint)
- [Web Sender Apps](Web Sender App Development)
- [Android Sender Apps](Android Sender App Development)
- [iOS Sender Apps](iOS Sender App Development)
- [Receiver Apps](Receiver Apps Development)
- Chromecast App Porting
- [API Libraries](API Libraries)
- [Flint Protocol Docs](Flint Protocol Docs)
- [Developer's Guide](Developer Guide for Flint)
-
Matchstick
- [Flashing Instructions](Flashing Instructions for Matchstick)
- [Build Your Matchstick](Build Your Matchstick)
- [Flashing Your Build](Flashing Your Build)
- [Setup Your Matchstick](Setup Your Matchstick)
- [Setup USB Mode](Setup USB Mode for Matchstick)
- [Supported Media](Supported Media for Matchstick)