Android WearのData Layer APIを試してみた

Android WearのDataLayerAPIを試してみました。 handheld(スマホとかタブレット)とwearable(時計)間でデータをやり取りするためのAPIです。

ドキュメントを読むと大きくわけて以下の2種類の方法があるみたいです。

  • Data Items
    • Wearable.DataApiクラスを使う
    • DataItemクラスを使ってデータを自動的にhandheldとwearable間でデータの同期ができる
  • Messages
    • Wearable.MessageApiクラスを使う
    • 片方向の通信でデータはメッセージといっしょに送ってもいいし送らなくてもいい

データ更新時やメッセージ受信時のハンドリングは、WearableListenerServiceを使うかActivityでリスナーを登録する方法の2通りがあるようですが、今回はActivityでリスナー登録する方法で試しました。

試しに実装したコードは以下にあります。
https://github.com/bati11/wear-datalayer-sample

参考にしたドキュメント

しばらく触らなさそうな気がするので忘れないうちにメモ。 以下はAndroid Studio 0.8.1で新規プロジェクトを作ってからの例です。

Data Itemsを使った実装

android.net.Uriで表される場所にデータを保存できるような感じ。同じURIをhandheldでもwearableでも使うことで同期されたデータを使える。URIはデータを保存というか同期して初めて分かる(作られる)ようになってるっぽいので、データを更新する側のアプリはいいとして、もう一方のアプリはデータの更新をリスナーで処理してURIもしくはデータ自体を取得する処理を書く必要がありそう
2014/8/2追記
URIが分かってなくてもpathが分かれば以下のようにしてデータを取得することができました。

PendingResult<DataItemBuffer> dataItems = Wearable.DataApi.getDataItems(mGoogleApiClient);
dataItems.setResultCallback(new ResultCallback<DataItemBuffer>() {
    @Override
    public void onResult(DataItemBuffer dataItems) {
    for (DataItem dataItem : dataItems) {
        if (dataItem.getUri().getPath().equals("/path")) {
            DataMap dataMap = DataMap.fromByteArray(dataItem.getData());
            // データを使った処理

追記ここまで。

wearableでデータを更新し、handheldでデータの更新を検知してハンドリングする流れを試した(逆でもコードはいっしょになる、はず...)。

wearable(データを更新する側)

step1

まずはAndroidManifest.xmlを編集する。以下を<application>タグ内に記述する。

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

Data Layer APIを使うにはGoogleApiClientインスタンスが必要になる。ActivityのonCreateで以下のように生成する。

mGoogleApiClient = new GoogleApiClient
    .Builder(this)
    .addConnectionCallbacks(this)
    .addOnConnectionFailedListener(this)
    .addApi(Wearable.API)
    .build();

addConnectionCallbacksメソッドGoogle Play servicesに接続できたときのリスナーを登録する。ActivityにGoogleApiClient.ConnectionCallbacksインタフェースを実装させ、onConnectedメソッドとonConnectionSuspendedメソッドをオーバーライドする。

同じようにaddOnConnectionFailedListenerメソッドGoogle Play servicesに接続失敗したときのリスナーを登録する。GoogleApiClient.OnConnectionFailedListenerを実装しonConnectionFailedメソッドをオーバーライドする。

そして、onResumeメソッドでGoogleApiClientインスタンスのconnectメソッドを呼ぶ。onPauseメソッドでdisConnectする。ここまででActivityは以下のようになる。

public class WearActivity extends Activity
        implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
    private GoogleApiClient mGoogleApiClient;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_data_item_sample);

        mGoogleApiClient = new GoogleApiClient
                .Builder(this)
                .addConnectionCallbacks(this)
                .addApi(Wearable.API)
                .build();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGoogleApiClient.connect();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
            mGoogleApiClient.disconnect();
        }
    }

    @Override
    public void onConnected(Bundle bundle) {
        Log.d("TAG", "onConnected");
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.d("TAG", "onConnectionSuspended");
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Log.e("TAG", "onConnectionFailed: " + connectionResult);
    }
}
step3

ボタンのonClick時にonClickメソッドが実行され、そのタイミングでデータを更新するとする。
データの入れ物がDataItemクラスだが、それを使いやすくしたDataMapクラスを使う。PutDataMapRequest.createメソッドで新たなDataMapインスタンスを用意する。このときDataMapを一意に特定できるようにパスを指定する。パスはスラッシュで始める必要がある。 生成したDataMapインスタンスにデータをつめて、DataApiを使って更新する。

public void onClick(View v) {
    // DataMapインスタンスを生成する
    PutDataMapRequest dataMapRequest = PutDataMapRequest.create("/datapath");
    DataMap dataMap = dataMapRequest.getDataMap();

    // データをセットする
    dataMap.putInt("key", data);

    // データを更新する
    PutDataRequest request = dataMapRequest.asPutDataRequest();
    PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(mGoogleApiClient, request);
    pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
        @Override
        public void onResult(DataApi.DataItemResult dataItemResult) {
            Log.d("TAG", "onResult: " + dataItemResult.getStatus());
        }
    });
}

handheld(データの更新を検知する側)

step1,2については同じことをしてGoogleApiClientのconnectメソッドを呼ぶ。

step3

connectが成功するとGoogleApiClient.ConnectionCallbacksインタフェースを実装してオーバーライドしたonConnectedメソッドが呼ばれる。更新を検知して処理をしたい場合は、DataApiに対してonConnectedメソッドでListenerを登録する。onPauseメソッドでListenerを削除する。

@Override
public void onConnected(Bundle bundle) {
    Wearable.DataApi.addListener(mGoogleApiClient, this);
}
@Override
protected void onPause() {
    super.onPause();
    if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
        Wearable.DataApi.removeListener(mGoogleApiClient, this);
        mGoogleApiClient.disconnect();
    }
}

Listenerとして登録するために、ActivityにDataApi.DataListenerインタフェースを実装させ、onDataChangedメソッドをオーバーライドする。データが更新されると、onDataChangedメソッドが実行される。

@Override
public void onDataChanged(DataEventBuffer dataEvents) {
    for (DataEvent event : dataEvents) {
        // TYPE_DELETEDがデータ削除時、TYPE_CHANGEDがデータ登録・変更時
        if (event.getType() == DataEvent.TYPE_DELETED) {
            Log.d("TAG", "DataItem deleted: " + event.getDataItem().getUri());

        } else if (event.getType() == DataEvent.TYPE_CHANGED) {
            Log.d("TAG", "DataItem changed: " + event.getDataItem().getUri());

            // 更新されたデータを取得する
            DataMap dataMap = DataMap.fromByteArray(event.getDataItem().getData());
            data = dataMap.getInt("key");
        }
    }
}

Messagesを使った実装

Messagesは方方向のデータ通信。データを更新・同期するという考え方ではなく、その名の通りメッセージを送信するという考え方。 DataApiの場合と実装方法は結構似てる。

メッセージ送信側

step1,2についてはDataApiの場合と同じことをしてGoogleApiClientのconnectメソッドを呼ぶ。

step3

ボタンのonClick時にonClickメソッドが実行され、そのタイミングhandbeltへメッセージを送るとする。
DataApiのときと違うのが送信先を示すNodeクラスが登場するところ。メッセージを送る端末を選択できるみたい。NodeApiを使ってNodeインスタンスを取得する。
DataApiのときと同じなのがスラッシュで始まるパスでメッセージを識別するところ。以下の例ではデータもいっしょに送ってるけど(byte配列)、送らなくてもいい。
Nodeを取得する処理も非同期なのでコールバック処理が重なって見づらい・・・。工夫する余地きっとあるけどまた今度。。

public void onClick(View view) {
    PendingResult<NodeApi.GetConnectedNodesResult> nodes = Wearable.NodeApi.getConnectedNodes(mGoogleApiClient);
    nodes.setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
        @Override
        public void onResult(NodeApi.GetConnectedNodesResult result) {
            String messagePayload = "Hello!";
            for (Node node : result.getNodes()) {
                final byte[] bs = (messagePayload + " " + node.getId()).getBytes();
                PendingResult<MessageApi.SendMessageResult> messageResult =
                        Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/messagesample", bs);
                messageResult.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
                    @Override
                    public void onResult(MessageApi.SendMessageResult sendMessageResult) {
                        Status status = sendMessageResult.getStatus();
                        Log.d("TAG", "Status: " + status.toString());
                    }
                });
            }
        }
    });
}

メッセージ受信側

step1,2についてはDataApiの場合と同じことをしてGoogleApiClientのconnectメソッドを呼ぶ。

step3

DataApiのときの検知側とほぼいっしょ。メッセージを受信したい場合は、onConnectedメソッドでMessageApiに対してListenerを登録する。onPauseメソッドでListenerを削除する。

@Override
public void onConnected(Bundle bundle) {
    Wearable.MessageApi.addListener(mGoogleApiClient, this);
}
@Override
protected void onPause() {
    super.onPause();
    if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
        Wearable.MessageApi.removeListener(mGoogleApiClient, this);
        mGoogleApiClient.disconnect();
    }
}

Listenerとして登録するために、ActivityにMessageApi.MessageListenerインタフェースを実装させ、onMessageReceivedメソッドをオーバーライドする。

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {
        if (messageEvent.getPath().equals("/messagesample")) {
            String messagePayload = new String(messageEvent.getData());
            Log.d("TAG", messagePayload);
        }
    }