Skip to content

Android Sender App Development

ForMyHime edited this page Dec 11, 2014 · 4 revisions

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.

Setup

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.

Download Library

Library dependencies

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>.

Development

Android manifest

Your AndroidManifest.xml file requires the following configuration to use the MatchStick SDK.

uses-sdk

The minimum Android SDK version that the MatchStick SDK supports is 9 (GingerBread).

<uses-sdk
    android:minSdkVersion="9"
    android:targetSdkVersion="19" /> 

users-permission

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" />

service

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>

android:theme

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>

Typical sender application flow

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

Adding the Flint Button

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.

Handling device selection

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();
}

Launching the receiver

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();
    }
};

Custom channel

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.

Media channel

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());
        }
    }
});

Error handling

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;
}

Sample apps

Several Matchstick sample sender apps have been open sourced on GitHub.

  • Flint Videos Sample Sender for Android
  • 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)
  • 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)
Clone this wiki locally