C# + Xamarin + Message APIを使って、Android WearとHandheld間でメッセージの往復をする

XamarinでAndroid WearのMessage APIを使ってみましたが、いろいろとあって忘れそうだったので、長いメモを残します。

なお、Android WearをWear、Android Handheld(今回の場合はNexus7の実機)をHandheldと呼ぶことにします。

また、メソッドなどが属している名前空間で悩んだため、ソースコードにはできるだけ名前空間を付けました。

 

環境

 

アプリの流れ

Message APIを使って、以下のような機能の流れを持つアプリを作ります。

  1. Wearにて、Handheldへメッセージを送信
  2. Handheldにて、メッセージを受信
  3. Handheldにて、メッセージを受信したタイミングで、Wearへメッセージを送信
  4. Wearにて、メッセージを受信

 

Xamarin評価版へのアップグレード

Xamarin Starterの環境にて、Android Wear Applicationのプロジェクトテンプレートをビルドしようとすると、以下のエラーが出ます(手元の環境では一部文字化けしていました)。

C:\Program Files (x86)\MSBuild\Xamarin\Android\Xamarin.Android.Common.targets(253,5): mandroid error XA9005: User code size, 3290112 bytes, is larger than 131072 and requires aツIndieツ(or higher) License.

C:\Program Files (x86)\MSBuild\Xamarin\Android\Xamarin.Android.Common.targets(253,5): mandroid error XA9006: Using type `Android.Runtime.JNIEnv` requiresツIndieツ(or higher) License.

 
StarterではAndroid Wearで使うライブラリのサイズが大きくてビルドできないように見えました。

ただ、さすがにIndie以上のプランにアップグレードするのはいろいろと厳しいので、今回は評価版を使います。

なお、評価版へのアップグレードに少し悩みましたが、以下の記事を参考にしたらアップグレードできました。
Xamarin Studio Windows のみで Business 評価版を開始するには - Xamarin 日本語情報

 

Android SDK Manager によるインストール

Android Wearアプリを作成するのに必要なモノをインストールします。
Setup and Installation | Xamarin

 

空ソリューションの作成

深い意味はありませんが、今回はソリューション名とプロジェクト名を分けてみようと思い、その他 > 空のソリューションで空のソリューションを作ります。

 

Android Wear側プロジェクトのひな形を作成

プロジェクトテンプレート選択

ソリューションの追加から、C# > Android > Android Wear Applicationプロジェクト(今回のプロジェクト名はWear)を追加します。

なお、Android SDK Platformがインストールされていない場合、このテンプレートが表示されませんでした。

 

Android manifestの追加

プロジェクトを作成した時点ではAndroid manifestが存在しないため、追加します。

  • プロジェクト(Wear)の上で右クリックし、オプションを選択
  • ビルド > Android Application
  • 右側のペインの「Add Android manifest」ボタンをクリック
  • Android manifest が追加されたら、「OK」ボタンを押す

 

Android manifestの編集

今回使用するMessage APIGoogle Play Serviceを使いますが、デフォルトのままでは使用することができません。

そのため、以下の作業を行い追記します。

  • Propertiesの下にあるAndroidManifest.xmlを選択・表示
  • 下側にあるソースタブを押して、xmlソースを表示
  • Wearからメッセージを送信するため、<application android:label="Wear">タグに、以下を追加
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />

android - Adding Google Play services version to your app's manifest? - Stack Overflow

 

パッケージ名の変更

Message APIを使ってWearとHandheldで通信を行う場合、両プロジェクトで同じパッケージ名を使う必要があります。

そのため、AndroidManifest.xmlを編集するか、以下の作業にてパッケージ名を変更します(Handheld側は後述)。

  • プロジェクトの上で右クリック、オプションを選択
  • ビルド > Android Application
  • Package nameを、XamarinWearableMessageApiSampleへと変更
    • 今回の場合、元々はWear.Wearとなっているはず

 
また、クラスの名前空間(namespace)やApplication nameは特に影響しないので、元のままにしておきます。

 

Wearアプリに、WearからHandheldへ送信する機能を追加

必要なインタフェースを追加

Android.Gms.Common.Apis名前空間にあるインタフェースを3つ、MainActivityに実装します。

今のところは特に使わないので、空実装にしておきます。

public class MainActivity : Activity,
    Android.Gms.Common.Apis.IResultCallback,
    Android.Gms.Common.Apis.IGoogleApiClientConnectionCallbacks, 
    Android.Gms.Common.Apis.IGoogleApiClientOnConnectionFailedListener
{
    // IGoogleApiClientConnectionCallbacksインタフェース向け
    public void OnConnected (Android.OS.Bundle connectionHint)  {}
    public void OnConnectionSuspended (int cause) {}

    // IGoogleApiClientOnConnectionFailedListenerインタフェース向け
    public void OnConnectionFailed (Android.Gms.Common.ConnectionResult result) {}

    // IResultCallbackインタフェース向け
    public void OnResult (Java.Lang.Object result) {}
...
}

 

Google API Clientを追加

Message APIで使う、Google API Clientを追加します。

Clientはインスタンス変数として用意して、OnCreateの中でGoogleApiClientBuilderを使ってインスタンス化しています。

# MainActivityのインスタンス変数
Android.Gms.Common.Apis.IGoogleApiClient client;

protected override void OnCreate (Bundle bundle)
{
    base.OnCreate (bundle);
    // Set our view from the "main" layout resource
    SetContentView (Resource.Layout.Main);

    client = new GoogleApiClientBuilder (this)
        .AddApi (Android.Gms.Wearable.WearableClass.Api)
        .AddConnectionCallbacks (this) // Xamarinの場合はクラスがないので、このクラスのOnConnect系を使う
        .AddOnConnectionFailedListener (this) // 同上
        .Build ();
    client.Connect ();
...
}

 
今回は生成直後にConnect()メソッドを使っていますが、場合によっては別のところで記述します(後述の参考資料など)。

また、リスナーやコールバックをMainActivity自身に実装し、それを渡しています。今回の例ではリスナーやコールバックを使っていないため、削除しても良いかもしれません。

 

WearableClass.MessageApi.SendMessageの実装前に考えたこと

WearableClass.MessageApi.SendMessageを実装する場合、送信先のNodeIDを取得する必要があります。

それを

var nodes = WearableClass.NodeApi.GetConnectedNodes(client).Await().JavaCast<INodeApiGetConnectedNodesResult>();

のようにしてUIスレッド上で取得しようとすると、以下のようなエラーが発生します。

[MonoDroid] UNHANDLED EXCEPTION:
[MonoDroid] Java.Lang.IllegalStateException: Exception of type 'Java.Lang.IllegalStateException' was thrown.
...
[MonoDroid] java.lang.IllegalStateException: await must not be called on the UI thread

 
そのため、非同期で取得する必要がありますが、今回は以下の2つの方法を試してみます。

  • Java同様、Android.OS.AsyncTaskを継承したクラスを使う方法
  • C#のasync/awaitを使う方法

 

MainActivityのLayoutを変更

2つの方法を実装する前に、各方法でメッセージを送信できるようボタンを2つ用意し、クリック時にそれぞれの方法で送信できるようにレイアウトを変更します。

 

Layoutの修正

layoutディレクトリの下に3ファイルありますが、関係するのは以下の2つになります。

  • RectangleMain.axml
  • RoundMain.axml

今回のエミュレータはSquare型のSkinにするためRectangleMain.axmlを修正しますが、念のためRoundMain.axmlも書いておきます。なお、内容は両方とも一緒です。

<LinearLayout
...
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/AsyncAwait"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/async_await" />
        <Button
            android:id="@+id/AsyncTask"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/async_task" />
    </LinearLayout>
</LinearLayout>

 
合わせて、ボタンにテキストを付けるために、values\String.xmlも追記します。

<resources>
...
    <string name="async_await">await</string>
    <string name="async_task">task</string>
...
</resources>

 

AsyncTaskによるWearableClass.MessageApi.SendMessageの実装
Android.OS.AsyncTaskを継承したクラスを作成

Javaと同じように、SendMessageAsyncTaskクラスを作成します。

このクラスは、

  • Android.OS.AsyncTaskを継承
  • 対象のActivityを入れておくActivityプロパティを用意
  • DoInBackgroundメソッドにて、Activityに用意したUIスレッドで動かせないメソッドを実行

という内容を実装します。

 

public class SendMessageAsyncTask : Android.OS.AsyncTask
{
    public MainActivity Activity { get; set; }

    protected override Java.Lang.Object DoInBackground (params Java.Lang.Object[] parameters)
    {
        if (Activity != null) {
            var nodes = Activity.NodeIds;
            foreach (var node in nodes) {
                Activity.SendMessage (node);
            }
        }
        return null;
    }
}

 

MainActivityに実装を追加

SendMessageAsyncTaskで必要なプロパティ(NodeIds)とメソッド(SendMessage)を追加します。

NodeIdsプロパティでは、送信先のNodeIdを列挙して返すようにします。

なお、そのままではINodeApiGetConnectedNodesResult型として取得できないことから、JavaCastを使用してキャストしておきます。

public ICollection<string> NodeIds {
    get {
        var results = new HashSet<string> ();
        var nodes =
            Android.Gms.Wearable.WearableClass.NodeApi.GetConnectedNodes (client)
                .Await ()
                .JavaCast<Android.Gms.Wearable.INodeApiGetConnectedNodesResult>();

        foreach (var node in nodes.Nodes) {
            results.Add (node.Id);
        }
        return results;
    }
}

 
SendMessageメソッドでは、列挙されたNodeIdに対してメッセージを送信します。

MessageTagは送受信側で同じ値を使えば、メッセージを識別することができます。

また、送信データはバイト配列にする必要があるため、適当なエンコーディング(今回はUTF8)を使ってバイト配列にします。

private const string MessageTag = "hoge";

public void SendMessage(String node) {
    WearableClass.MessageApi.SendMessage (client, node, MessageTag, System.Text.Encoding.UTF8.GetBytes ("async_task"));
}

 
以上が、Android.OS.AsyncTaskを使った場合の送信方法となります。

 

C#のasync/awaitによるWearableClass.MessageApi.SendMessageの実装

C#っぽくasync/awaitを使います。今回はいずれもMainActivityに実装します。

 
まず、前述のNodeIdsプロパティでawaitできるよう、TaskでラップしたGetNodeIdsAsyncメソッドを用意します。

public Task<ICollection<string>> GetNodeIdsAsync()
{
    return Task.Run(() => NodeIds);
}

 
次に、awaitするSendMessageAsyncメソッドを用意します。

public async void SendMessageAsync()
{
    var nodeIds = await GetNodeIdsAsync();

    foreach (var nodeId in nodeIds) {
        WearableClass.MessageApi.SendMessage(client, nodeId, MessageTag, Encoding.UTF8.GetBytes("async_await"));
    }
}

 
以上が、C#のasync/awaitを使った場合の送信方法となります。

 

ボタンクリック時の動作を追加

レイアウトに追加したボタンを押した時に、メッセージ送信機能が実行されるように実装します。

Xamarinではいくつかの書き方があるようですが、今回はラムダ式にて実装します。
Handle Clicks - Xamarin

// C#のasync/awaitを使う方法
var asyncAwait = FindViewById<Button> (Resource.Id.AsyncAwait);
asyncAwait.Click += (sender, e) => SendMessageAsync ();

// AndroidのAsyncTaskを使う方法
var asyncTask = FindViewById<Button> (Resource.Id.AsyncTask);
asyncTask.Click += (sender, e) => 
{
    var task = new SendMessageAsyncTask(){ Activity = this };
    task.Execute();
};

 
ここまでの内容は以下のコミットになります。
Add send message function to Wear project - thinkAmi-sandbox/XamarinWearableMessageApi-sample · GitHub 

 

Android Handheld側プロジェクトのひな形を作成

Wear側のメッセージの送信機能ができたので、今度は受信側であるHandheld側を実装します。

 

Android Applicationプロジェクトの追加

ソリューションにAndroid Applicationプロジェクトを追加します。今回は「Handheld」と名づけます。

 

AndroidManifest.xmlの追加

Wearと同様にして、AndroidManifest.xmlを追加します。

 

Xamarin.GooglePlayServicesNuGetパッケージの追加

Handheld側は普通のAndroid Applicationなため、このままではGoogle Play Serviceを使うことができません。

そのため、NuGetパッケージのXamarin.GooglePlayServicesを追加します。なお、Google Play ServicesはNuGetとXamarin Componentの2つがありますが、NuGetのほうがバージョンが微妙に新しいのと、以下の記事があったので、NuGetから導入しました。

Xamarin用のGoogle Play ServicesはComponentsでも提供されている。現在のところ両者に違いはないようだが、最近のXamarinのオンラインセミナーでは「Google Play ServicesはNuGetで提供される」と紹介されているので、ここではNuGetを使う方法を解説した。

Xamarin.Androidで地図を表示するには?(Google Maps使用) - Build Insider

 
通常だとXamarin Studioでは

  • プロジェクトを選択して右クリック
  • 追加 > Add Nuget Packages... を選択
  • 右上の検索欄で、Xamarin.GooglePlayServicesを入力
  • Xamarin Google Play Services Bindingにチェックを入れ、「Add Package」ボタンを押す

のようにしてNuGetから導入します。

 

現在の最新版(v22.0.0.2)への対応

ただ、Xamarin.GooglePlayServicesの最新版(v22.0.0.2)を入れると、ビルド時に100個ほど以下のようなコンパイルエラーが発生するようになります。

%USERPROFILE%\AppData\Local\Xamarin\Android.Support.v7.AppCompat\21.0.3\embedded\.\res\values-v21\values.xml(0,0): Error: Error retrieving parent for item: No resource found that matches the given name 'android:TextAppearance.Material'. (Handheld)

 
以下によると、原因はビルドツールのせいのようです。そのため、ビルドのバージョンを5.0(API 21)へと変更します。
Xamarin.Android.Support.v7.AppCompat利用時のビルドエラー - 日々のアレコレ(2014-12-25)

 
再度ビルドすると、以下のコンパイルエラーが出ました。

C:\Program Files (x86)\MSBuild\Xamarin\Android\Xamarin.Android.Common.targets(2,2): Error: Could not find android.jar for API Level 21. This means the Android SDK platform for API Level 21 is not installed. Either install it in the Android SDK Manager (Tools > Open Android SDK Manager...), or change your Xamarin.Android project to target an API version that is installed. (%USERPROFILE%\AppData\Local\Android\android-sdk\platforms\android-21\android.jar missing.) (Handheld)

 
Android SDKAPI 21をインストールしていないことから、Android 5.0.1 (API 21)のSDK Platformのみを追加でインストールします。

 

実機のNexus7 (Android 4.4)への対応

この状態で実機のNexus7(Android 4.4)へデプロイしようとすると、以下のエラーになります。

Deployment failed because the device does not support the package's minimum Android version. You can change the minimum Android version in the Android Application section of the Project Options.

Deployment failed. Minimum Android version not supported by device.

 
おそらく、ビルドのバージョンをAPI21にしたため、現在のMinimum Android Versionの設定(Automatic)もAPI21相当になったものと考えられます。

そのため、AndroidManifest.xmlを開き、Minimum Android VersionをAutomaticからOverride - Android 4.4 (API level 19)へと変更します。

 

ヒープに関するエラーへの対応

再度ビルドすると、別のコンパイルエラーが一つ出ます。

path\to\project\COMPILETODALVIK: Error:  (Handheld)

 
そのため、stackoverflowの回答に従い、以下の作業を行います。
c# - Java heap space OutOfMemoryError when binding a .jar in Xamarin - Stack Overflow

  • プロジェクトで右クリック、オプションを選択
  • ビルド > Android Build を選択
  • Advancedタブにある、Java heap sizeに 1G を入力し、OKボタンを押す

 
再度ビルドをすると、成功します。

 

パッケージ名の変更

Wear側同様、Handheld側もパッケージ名をWearと共通のものに変更します。今回は「MessageApiSample」となります。

 

Serviceとしてメッセージ受信機能を作成

通常のActivityとしてメッセージ受信機能を実装してもよいのですが、せっかくなのでService(今回は「MessageService」という名前)として実装してみます。

大まかな作り方はWearのMainActivityと同じですが、以下の様な点が異なります。

  • 空のクラスのテンプレートを元に、必要な機能を追加
  • Android.Gms.Wearable.WearableListenerServiceを継承したクラスに実装
  • 属性として、以下の2つを追加
    • [Android.App.Service()]
    • [Android.App.IntentFilter(new string[] { "com.google.android.gms.wearable.BIND_LISTENER" }) ]
  • 受信時の処理はOnMessageReceivedをオーバーライド実装
  • メッセージの受信だけであれば、OnCreate内でGoogleApiClientBuilderによる生成は不要

また、受信データはToastを使って表示します。

 
MessageServiceの実装は以下のコミットのMessageService.csになります。ソースコードが長いので、ここでは省略します。
Add receive message function to Handheld project - thinkAmi-sandbox/XamarinWearableMessageApi-sample · GitHub

 

WearからHandheldへのテスト送信

テスト送信をするために、WearとHandheldへアプリをインストールします。

以下の方法でインストールしますが、いろいろと面倒なため、他の方法があるのかもしれません。ご存じの方は教えてくださるとありがたいです。

 

Handheldへのインストール

以下の流れになります。

  • Handheldプロジェクトの上で右クリック
  • アプリケーションを選択して開く > Nexus7(実機) を選択

実機を選択すると、ビルドが走り、Nexus7へアプリがインストールされ、Xamarin Studioにも「Deployment completed」と表示されます。

そのままだとアプリの実行が継続するため、停止ボタンで実行を止めます。

 

Wearへのインストール
WearのエミュレータとNexus7の接続

すでにペアリングをしている場合は不要ですが、行っていない場合は以下を参考にペアリングをします。
【Android Wear】Android SDKを使ったエミュレータでPCでAndroid Wearを試す…2014/7/12更新 | AndMem - Androidのカスタマイズなど

  • Nexus7で、Android Wearアプリを起動
    • インストールしていない場合は、Google Playからダウンロード・インストール
  • Android Wearアプリで、「端末を選択」と表示されているのであれば、右上のメニューより「エミュレータをペア設定」を選択
  • Android Wearアプリで、左上が「エミュレータ 接続済」となっていることを確認

 

デバッグ実行

スタートアッププロジェクトが「Wear」となっていることを確認します。

次に、Debug・VirtualDeviceでWearエミュレータを選択し、デバッグ実行します。

しばらくするとAndroid Wearに以下のような画面が表示されます。 f:id:thinkAmi:20150306051828p:plain

 
そこで、それぞれのボタンをタップすると、Nexus7に選択した方のメッセージ内容がToastで表示されます。

awaitボタンをタップ

f:id:thinkAmi:20150306051846p:plain

 

taskボタンをタップ

f:id:thinkAmi:20150306051950p:plain

 

HandheldからWearへメッセージ送信 (Handheld側)

今度は、Wearからメッセージを受信したタイミングで、HandheldからWearへメッセージ送信を行います。

Wear側の送信処理と同様、

  • OnCreateでGoogleApiClientBuilderによる生成とConnectの実行を追加
  • OnMessageReceivedにて、メッセージを送信するコードを追加
  • JavaCastメソッドを使うために、usingディレクティブにAndroid.Runtime名前空間を追加

を行います。

var nodeIds = NodeIds;
foreach (var nodeId in nodeIds)
{
    WearableClass.MessageApi.SendMessage(client, nodeId, MessageTag, System.Text.Encoding.UTF8.GetBytes("こんにちは"));
}

 
なお、AndroidManifest.xmlへの

<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />

の追加は不要でした。

また、ServiceはUIスレッドはないので、NodeIdを取得する時にasync/awaitとかも不要でした。

 

HandheldからWearへメッセージ送信 (Wear側)

MainActivityのOnMessageReceivedにてメッセージを受信する処理を実装します。

MainActivityに追加する内容としては以下となります。

  • 属性として[Android.App.IntentFilter(new string[] { "com.google.android.gms.wearable.BIND_LISTENER" })]を追加
  • Android.Gms.Wearable.IMessageApiMessageListenerインタフェースを追加
  • インタフェース実装のOnMessageReceivedを追加し、受信処理を実装
  • OnConnectedに、リスナーを追加するコードを実装
public void OnConnected (Android.OS.Bundle connectionHint)
{
    Android.Gms.Wearable.WearableClass.MessageApi.AddListener (client, this);
}

 
この部分も長いので、MainActivityのソースコードは以下のGitHubリンクで確認ください。

 

テスト実行

先ほどと同じように実行します。

f:id:thinkAmi:20150306052029p:plain

Handheldから送信した「こんにちは」という文字が、Wearで受信・Toast表示できました。

 

ソースコード

GitHubに置いておきました。
thinkAmi-sandbox/XamarinWearableMessageApi-sample · GitHub

 

参考

Message APIに関係する公式情報

 

WearとHandheld間の通信に関して

 

WearableListenerServiceを使ったり、接続/切断を意識しているサンプル

 

Android Wearのサンプル

 

AndroidのAsyncTask

 

C#のasync/await

 

お知らせ

GDG信州によるAndroid Wearに関するイベントが、3/14に開かれます。
GDGShinshu AndroidWear Event in WhiteDay on Zusaar

Android Wearが気になる方の参加をお待ちしています!