Android Wear2.0 Watch face Complicationsを作るDataSync編

2016/10/12 1781hit

前回はWear単独で動いてランダムな値を表示するだけのWatch face Complicationを作成しましたが、もちろんこれでは何の訳にもたちません。
そこで今回はより実践的なスマートフォン上で入力したデータをWatch face Complicationに表示する機能を追加してみます。
スマートフォン側の処理を作り変えると天気予報やツイート数など好きな情報をWatch face Complicationに表示できるようになります。

目次

基礎
DataSync
多データ型対応

全体の流れ

スマートフォンのActivityからWearのWatch face complicationにデータを送るには次のような手順をふみます。
1.スマートフォンのActivityがWearにメッセージを送信。
2.WearのWearableListenerServiceがメッセージを受信。
3.WearableListenerServiceがシステムにWatch face Complicationの更新を要求。
4.システムがリクエストを元にProvider serviceにデータを要求。
5.ComplicationProviderServiceが最新のデータをcomplicationDataにつめてシステムに渡す
6.システムがWatch faceに最新のデータを渡す
7.Watch faceがWatch face Complicationを表示する


このうちData provider側で作成する必要があるのが1〜3と5、Watch face側で作成する必要があるのが7です。
それでは実際にデータの流れに沿ってプログラムを作っていきましょう。

メモ:Android Wear2.0 Preview3はその名の通りPreview版です。
挙動面に不安定な部分があり、正常にコーディングしていても正しくWatch faceが描画されなかったり、画面が反応しなくなることが有ります。
そのような時にはまずWearを再起動してみてください。

1.スマートフォンのActivityからWearにメッセージを送信


まずはスマートフォンのActivityを作っていきましょう。
NumberPickerの値が変更されるとWearに値をメッセージとして送信する処理を実装します。

build.gradleの編集

mobileモジュールのbuild.gradleを開いてください。

スマートフォン側でWearと連携するにはgms:play-services-wearableを使用します。
標準のテンプレートですでにwearableを含むcom.google.android.gms:play-services付与されています。このまま使用してもいいのですが、com.google.android.gms:play-servicesは多機能すぎてメソッド数が多すぎmulti-dexが必要だったりビルドに時間がかかりすぎたりするため今回はwearable以外の機能を使用しないようにします。
mobile/build.gradleのdependenciesにある

compile 'com.google.android.gms:play-services:9.6.1'



compile 'com.google.android.gms:play-services-wearable:9.6.1'

に変更します。

layout.xmlの編集

ここは実際のアプリではそのまま使うことはないと思うので、細かな説明は省略します。
単にNumberPickerを配置しているだけです。
layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.firespeed.dataproviders.MainActivity">

<NumberPicker
android:id="@+id/numberPicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
</FrameLayout>



MainActivity.javaの編集

AndroidWearへのデータ送信を行う時にはGoogleApiClientを使用します。
GoogleApiClientを使ったデータの送信にはDataSyncとMessageという2つの方法があります。
DataSyncはそれぞれの端末内でデータを保持し、変更があった場合に互いに差分データを送りつけてお互いのデータを一致させるような仕組みです。
スマートフォンでもスマートウォッチでもどちらでも単独で動ける万歩計など、スマートフォンとスマートウォッチそれぞれが同一のデータを別々に更新する場合などに便利です。

それに対してMessageはデータを一方的に送りつけるイメージ。データの送信が行えないときはそのままエラーとなります。
構造がシンプルな分、実装も簡単になります。
また、Preview3の段階ではDataSyncは不定期にデータを受信しなくなる不具合が残っていてMessageのほうが安定しています。
今回はNumberPickerで選んだ値を一方的にスマートウォッチに送りたいためMessageを使用します。

Viewの取得とイベントハンドラの作成

onCreate()内でnumberPickerを取得して値変更時のリスナーを登録します。一般的な処理です。
MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NumberPicker numberPicker = (NumberPicker)findViewById(R.id.numberPicker);
numberPicker.setMaxValue(99);
numberPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {

}
});

}


GoogleApiClientの作成と接続、切断

インスタンス変数としてGoogleApiClientを宣言します。
MainActivity.java

private GoogleApiClient mGoogleApiClient;


onCreate()時にGoogleApiClientのインスタンスを生成します。
GoogleApiClientのインスタンスはGoogleApiClient.Builderを使用します。
MainActivity.java

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


GoogleApiClientをonResume()時に接続し、
MainActivity.java

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



onPauseで切断します。
MainActivity.java

@Override
protected void onPause() {
super.onPause();
mGoogleApiClient.disconnect();
}


sendMessage()メソッドを作りメッセージの送信を実装していきます。
MainActivity.java

private void sendMessage(final int value){

}


Nodeの取得

データの通信を行うにはNodeを取得します。
Wearable.NodeApi.getConnectedNodes()を呼びNodeの一覧を取得します。
引数にGoogleApiClientを指定し、setResultCallback()を繋げてコールバックを設定することで非同期にノードを取得できます。
MainActivity.java

private void sendMessage(final int value) {
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(@NonNull NodeApi.GetConnectedNodesResult getConnectedNodesResult) {
// ノードを取得

}
});

}


データの送信

データの送信はbyteの配列で通信を行う必要があるため、intをbyte配列に置き換えます。
MainActivity.java

byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();


取得した各ノード全てと通信を行うためにgetConnectedNodesResult.getNodes()の内容文forを回してWearable.MessageApi.sendMessage()を呼びます。
sendMessage()の引数は最初にGoogleApiClient、次にnodeのID、パス、送信するバイト配列、送信完了時の成否を受け取るコールバックをわたします。
データの通信は非同期で行われ通信が終わるとSendMessagerResultのonResult()が呼ばれます。

for (Node node : getConnectedNodesResult.getNodes()) {
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/path", bytes)
.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult sendMessageResult) {
}
});

}


sendMessage()全体は次の通り
MainActivity.java

private void sendMessage(final int value) {
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(@NonNull NodeApi.GetConnectedNodesResult getConnectedNodesResult) {
// ノードを取得
byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
for (Node node : getConnectedNodesResult.getNodes()) {
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/path", bytes)
.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult sendMessageResult) {
}
});

}
}
});

}

最後にonCreate()のnumberPickerにValueChangedListenerを設定し値が変更された時にsendMessage()を呼ぶようにしましょう。
MainActivity.java

numberPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
sendMessage(newVal);
}
});



MainActivity.java全体は次の通り

package org.firespeed.dataproviders;

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.widget.NumberPicker;

import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;

import java.nio.ByteBuffer;

public class MainActivity extends AppCompatActivity {


private GoogleApiClient mGoogleApiClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NumberPicker numberPicker = (NumberPicker) findViewById(R.id.numberPicker);
numberPicker.setMaxValue(99);
numberPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
sendMessage(newVal);
}
});
mGoogleApiClient = new GoogleApiClient
.Builder(this)
.addApi(Wearable.API)
.build();
}

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

@Override
protected void onPause() {
super.onPause();
mGoogleApiClient.disconnect();
}

private void sendMessage(final int value) {
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
@Override
public void onResult(@NonNull NodeApi.GetConnectedNodesResult getConnectedNodesResult) {
// ノードを取得
byte[] bytes = ByteBuffer.allocate(4).putInt(value).array();
for (Node node : getConnectedNodesResult.getNodes()) {
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/path", bytes)
.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult sendMessageResult) {
}
});

}
}
});

}
}


AndroidManifestの修正

AndroidManifestのApplication内にGooglePlayServicesのバージョンメタデータを追加します。
AndroidManifest.xml

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

2.WearのWearableListenerServiceがメッセージを受信。

送信されたデータをWearで受信する仕組みを作ります。
メッセージが飛んできた時に受信側ではIntentが飛ぶためIntentFilterでこれをキャッチしてServiceで処理を行います。

メモ:
MessageApiから送られてくるメッセージを受信するには送信側と同様にGoogleApiClientを繋いでWearable.DataApi.addListener()にリスナーをセットすることでも受け取ることが出来ますが、Wearのリソースは限りが有りComplicationProviderServiceのライフサイクルが短いことに注意が必要です。
Watch face ComplicationsにとってData providerはデータを提供するだけの役目で実際の描画はWatch faceが行っているため、データの提供が終わったData providerのServiceは役目を終えて自身のWatch face Complicationが表示されてある時であってもサービスが終了します。
そのためComplicationProviderServiceのonAtacch()やonCraeate()でGoogleApiClientを接続してもサービスが停止して切断されてしまいデータの更新を受け取ることは出来ません。
常時Serviceを起動しておくという方法もありますが、BackgroundのServiceはリソースが不足すると停止させられてしまうためこれも確実ではありません。
そのため、intentFilterを設定し、データの更新が起きたときだけServiceを起動し、速やかに値を保存、Watch faceのComplication更新を要求してServiceを終了します。

ReceiverService.javaの作成

Wearモジュールを開いて
WearableListenerServiceを継承したReceiverServiceクラスを作ります。
WearableListenerServiceはWearable.MessageApiで受信したデータを簡単に取り扱う仕組みでこれを継承することでMessageやDataSyncのイベントを簡単に受信できます。

ReceiverService.java

/**
* mobileからのメッセージを受信する
*/
public class ReceiverService extends WearableListenerService{
}

メッセージを受信したときはonMessageReceived()が呼ばれるためこのメソッドをOverrideしてメッセージ受信時の処理を記載します。
ReceiverService.java

@Override
public void onMessageReceived(MessageEvent messageEvent) {
super.onMessageReceived(messageEvent);
}


送信されたデータはmessageEventのgetData()を呼ぶことで取得できます。
byteの配列で送られてきているのでこれをintに変換します。

int value = ByteBuffer.wrap(messageEvent.getData()).getInt();


Android manifestの修正

次にAndroid manifestにServiceを宣言しIntent filterを設定します。
pathPrefixにはWearable.MessageApi.sendMessage()で指定したパスを記入します。

AndroidManifest.xml

<service android:name=".ReceiverService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/path"
android:scheme="wear" />
</intent-filter>
</service>

メモ:以前のドキュメントなどでIntentFilterにBIND_LISTENEを指定するものが有りましたが、こちらのAPIは何か一つのデータを送るとすべてのサービスが呼び出されてしまう効率が悪くServiceが起動中に停止されるおそれがある作りとなっており現在では非推奨となっています。MESSAGE_RECEIVEDを使用してください。

これでスマートフォンで操作した値をWearに送り届けることが出来るようになります。
ここまでの処理に関してはWatch face Complicationsに限らず、Wearのアプリを作るのによく使われる一般的な手法となります。

3.WearableListenerServiceがシステムにWatch face Complicationの更新を要求。

これでWearはデータの変更を受信できるようになりましたが現在のままでは受信したデータは変数valueにセットされたままでなにも使われることがありません。
最新のデータが有ることをシステム側に通知してWatch faceにWatch face complicationを更新するように通知します。
データの更新をリクエストしても実際にデータを更新するまでの間タイムラグが発生することが有ります。
それまでの間Serviceを起動したままにするのは非効率なのでデータを保存してServiceを終了し、最新の値は最新データが要求された時に保存した値を読み込むことにします。

データの保存は好きな方法でいいです。今回はSharedPreferencesを使います。

SharedPreferences data = getSharedPreferences("pref", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = data.edit();
editor.putInt("value", value);
editor.apply();

更新を要求するにはProviderUpdateRequesterクラスのインスタンスを使います。
コンストラクターとして第一引数にContext,第二引数に更新するComplicationProviderServiceのComponentNameを指定します。
ComponentNameのインスタンスを取得するために、ComponentNameのコンストラクターを呼びます。
第一引数にContext,第二引数にるComplicationProviderServiceのクラスを渡します。
ProviderUpdateRequesterのインスタンスにrequestUpdateAll()を呼ぶことでデータの更新が要求されます。

new ProviderUpdateRequester(this, new ComponentName(this, SampleComplicationProviderService.class)).requestUpdateAll();


メモ requestUpdate()を呼び、complicationIdを引数に渡すことで特定のComplicationのみを更新することが出来ます。
古い非公式のドキュメントなどにおいて、全てのWatch face complicationsを更新したい時にrequestUpdateAll()を呼ぶのではなく、complicationIdを全て記憶しておき、繰り返しrequestUpdate()を呼ぶ方法が使われている場合があります。
これはAndroid Wear2.0 preview2のバグでrequestUpdateAll()が正常に動作しないことが有るという問題を回避するためのもので、preview3でこのバグは解決しrequestUpdateAll()が使用できるようになっています。
なお、requestUpdateAll()を指定しても更新されるのは指定したクラスWatch face Complicationsのみでその他のWatch face Complicationsは更新されません。

5.ComplicationProviderServiceが最新のデータをcomplicationDataにつめてシステムに渡す

データの更新をプログラム側で行うことになったのでAndroidManifestに設定していたandroid:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDSのandroid:valueを0に変更し定期的な更新をストップします。
wear/AndroidManifest.xml

<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="0" />


SampleComplicationProviderService内で最新のデータを詰めてシステムに渡します。
最新のデータはSharedPreferencesに保存したので、変数valueに乱数を設定していた部分をSharedPreferencesから読み込むように修正します。

SampleComplicationProviderService.java

SharedPreferences data = getSharedPreferences("pref", Context.MODE_PRIVATE);
int value = data.getInt("value",0);



全体では次の通り
SampleComplicationProviderService.java

package org.firespeed.dataproviders;

import android.content.Context;
import android.content.SharedPreferences;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationManager;
import android.support.wearable.complications.ComplicationProviderService;
import android.support.wearable.complications.ComplicationText;

import static android.support.wearable.complications.ComplicationData.TYPE_SHORT_TEXT;

/**
* Complication Watch Face DataProviderがデータを提供するためのサービス
*/
public class SampleComplicationProviderService extends ComplicationProviderService {
/**
* データを返してほしいときに呼ばれる
*
* @param complicationId 画面上に置かれたWatch Face Complicationごとに一意となるID
* @param type 表示するデータのType
* @param complicationManager ComplicationManager 結果を渡す
*/
@Override
public void onComplicationUpdate(int complicationId, int type, ComplicationManager complicationManager) {
SharedPreferences data = getSharedPreferences("pref", Context.MODE_PRIVATE);
int value = data.getInt("value",0);
if (type == TYPE_SHORT_TEXT) {
ComplicationData complicationData = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText(String.valueOf(value)))
.build();
complicationManager.updateComplicationData(complicationId, complicationData);
}

}

}


これでスマートフォン側のActivityのNumberPickerを選ぶとWatch face complicationの値が置き換わるようになります。
置き換えのタイミング自体はWatch faceに依存するため、即時に変更されるとは限りません。
特に画面が薄暗くなるambientモードのときは画面更新間隔が1分間ごとになるのでData providerがデータを提供してから実際に反映されるまで時間差が有ることも見ることが出来ます。


次回は複数のWatch face complication typeへの対応や多解像度に対応したアイコンを作ってみましょう。



前:Android Wear2.0 Watch face Complicationsを作る 基礎編 次:Android Wear2.0 Watch face Complicationsを作る 多データ型対応編

関連キーワード

[Android][Java][モバイル][IT][ウェアラブル]

コメントを投稿する

名前URI
コメント