0

WEB+DB Press vol.86でAndroid Wearについて書いた

数日前発売のWeb+DB Press vol.86でAndroid Wearについて書いた。

Moto360

Android Wearは去年秋から半年ぐらい使っていて、特に面白いと思った機能が「通知」だったので、今回は通知するアプリを作った。
歩行中などふだんスマホを見ないタイミングでもAndroidはけっこう通知をしていて、例えばGoogle Oneの機能では直前に検索した施設へ行けるバス停が近くにある時に時刻表が出ていたりとか、FireChatが「周囲にFireChatを使っている人がいます」とか、場所や状況に応じて通知してくる。でもスマホだけでは(外を歩いているから)バイブレーションでも通知音でも気がつかない。
Wearつけてると腕が振動して、しかも時計を見るジェスチャだけで確認できる。

ちょうど発売されたばかりのApple Watchもそんな感じなんじゃないかと思う。Apple Watchの実物見たこと無いけど。


通知

というわけで外を歩いてる時に通知してくれるもの実装するか〜〜となって、Google MapsとGeofenceから場所のリマインダを作れるアプリを作った。

https://github.com/shokai/Android-GMapStars

ラーメン屋とか調べたら忘れないようにGoogle Mapsで星つけておくんだけど、たまたま別の用事で近くを通りかかった時にニアミスしたまま通り過ぎていたりする事がけっこうある。星つけた後、Mapの共有メニューからGeofenceを登録できるようにして、後日近くに行った時にWearに通知がでて、そこからMapを開けるアプリを作る話を書いた。

なおWearアプリはWearにアプリを書き込んでWear上で実行するのと、スマホ本体の通知をWear用にカスタムして表示するものの2種類があって、今回は後者でやっている。


WEB+DB PRESS Vol.86
WEB+DB PRESS Vol.86
posted with amazlet at 15.04.23
結城 洋志 沖元 謙治 足永 拓郎 林 健太郎 大竹 智也 内田 誠悟 伊藤 直也 中山 裕司 hiroki.o 泉水 翔吾 佐藤 太一 高橋 俊幸 西尾 泰和 舘野 祐一 中島 聡 橋本 翔 はまちや2 竹原 麻植 泰輔
技術評論社
売り上げランキング: 402


1番ライフチェンジングなのは通知で、2番は音声入力だと思う。音声入力で実世界系の事しようとするとさらに別のガジェットが必要になるので見送った。個人的にはWearから音声入力でツイートできるtwitterクライアント作ってる。音声入力からの画像検索(ok google 「hogehoge 画像」でできる)も、ド忘れした物事を一瞬で調べれて脳が拡張されてる感じある。


あとWearの写真撮るとどうしても腕毛が気持ち悪いので剃ったりとか色々してみたけど、結局毛穴も気持ち悪いのであきらめた。

Moto360

0

DataItemでAndroidWearと本体のデータを共有する

Wearと本体でデータを共有するにはDataItemを使う。

DataItemはkey-valueストアで、1キーに100KBまで保存できる。文字列・数値だけでなく画像などのバイナリデータも保存できて、Bluetoothの帯域を適当に考慮して同期してくれるらしい。
具体的な処理は隠蔽されているがデータはGoogleのサーバーにも保存されているっぽい。
そのためGoogle API Clientとインターネットへの接続が必要。

key-valueは wear://<node_id>/<path> のようなURIの下に保存されるが、node_idは普通意識しないのでpathだけ指定して使う。


保存期限

いつまでデータが生きているのかは、ドキュメントに書かれていないのでわからない。
期限を設定するプロパティが無い事と、DataItemに書き込んでからスマホとWearを両方とも再起動してもデータは残っていたことから、まあずっと残ってるんじゃないかと思う。
SharedPreferenceみたいにも使えそう。

片方が生きてない時にDataItemに書き込んで、もう片方だけ起動したら読めるか?を試そうとしたけどWearはそもそもスマホがないとネットワーク接続できなかったから試せなかった。


使い方

“/testapp”というpathの中で、名前とウェブサイトをWearと本体間で共有する例


書き込み

GoogleApiClientを接続してからkey-valueを保存する
PutDataMapRequest mapReq = PutDataMapRequest.create("/testapp");
mapReq.getDataMap().putString("name", "shokai");
mapReq.getDataMap().putString("url", "http://shokai.org");
Wearable.DataApi.putDataItem(mGoogleApiClient, mapReq.asPutDataRequest());

読み出し(イベント)

Wearable.DataApiにDataApi.DataListenerを登録しておけばデータの変更が通知されてくる。

// Activity自身をlistenerとして登録
Wearable.DataApi.addListener(mGoogleApiClient, this);
// 解除
Wearable.DataApi.removeListener(mGoogleApiClient, this);

public class MainActivity extends Activity implements DataApi.DataListener {

// (略)

@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
DataItem item = event.getDataItem();
if (item.getUri().getPath().equals("/testapp")) {
DataMap dataMap = DataMapItem.fromDataItem(item).getDataMap();
String name = dataMap.getString("name"); // "shokai"
String url = dataMap.getString("url"); // "http://shokai.org"
}
} else if (event.getType() == DataEvent.TYPE_DELETED) {
// 削除イベント
}
}
}

1回書き込んだだけでも、なぜか連続で2回イベントが来る事があった。1回目は古いデータで、2回目は更新されたデータがくる。


読み出し

イベントではなく、現在の値をこっちから読みに行く方法は
Android WearのData Layer APIを試してみた – bati11's diary
に書いてあった。GoogleAPIClientを接続してから
Wearable.DataApi.getDataItems(googleClient)
.setResultCallback(new ResultCallback() {
@Override
public void onResult(DataItemBuffer dataItems) {
String name = null;
String url = null;
for(DataItem dataItem : dataItems){
if(dataItem.getUri().getPath().equals("/testapp")) {
DataMap dataMap = DataMap.fromByteArray(dataItem.getData());
name = dataMap.getString("name");
url = dataMap.getString("url");
}
}
if(name != null && url != null) {
Log.i("userinfo", "found");
}
else{
Log.i("userinfo", "not found");
}
dataItems.release(); // releaseしないとリークするという警告がでる
}
});
この方法で他のアプリケーションのデータも読めてしまうかと思ったけど、読めなかった。よかった。


削除

DataMapまるごとしか削除できない。保存する時はpath指定なのに削除はURI指定しなければならない。

Uriのnode_idは省略できるので、/testappに保存してるDataMapはwear:/testappを指定したら削除できた。
削除するとonDataChangedイベントのTYPE_DELETEDが発火する。

Wearable.DataApi.deleteDataItems(mGoogleApiClient, Uri.parse("wear:/testapp"))
.setResultCallback(new ResultCallback() {
@Override
public void onResult(DataApi.DeleteDataItemsResult deleteDataItemsResult) {
Log.i("deleteDataItems", deleteDataItemsResult.getNumDeleted() + "個削除した");
}
});
}

DataItemにはnullも書き込めるので、DataMapまるごと削除したくない時はnull埋めでもいいかもしれない。

0

Android WearからHTTPリクエストできない

Android Wearから音声入力でツイートできるTwitterクライアントをTwitter4jで作っているんだけど、Android Wearから直接はHTTPリクエストできなかった。

エミュレータからだとホスト名が解決できないし

Unable to resolve host “api.twitter.com”: No address associated with hostname


実機(Moto360)からだとホスト名は解決できてるけど接続ができない。

failed to connect to api.twitter.com/199.16.158.169 (port 443) after 20000ms


Twitter4jじゃなくて普通にHttpURLConnectionを使って他のサーバーにリクエストしても同じ結果になる。


調べてもコレぐらいしか困ってる人がいない。全員Wearから他のサーバーへは接続できなくて未解決
Does Android Wear support HttpURLConnection – getting EOFException – Stack Overflow


ドキュメントにはネットワーク使えないって書いてないけど、無理なものは無理なので次は設計時に気をつける
Creating Wearable Apps | Android Developers

Wearable apps can access much of the standard Android APIs, but don’t support the following APIs:
android.webkit
android.print
android.app.backup
android.appwidget
android.hardware.usb



解決方法

スマホ側をproxyにするしかない。

もともとWearからsendMessageしてスマホ側のWearableListenerServiceで受け取ってtweetを中継していたので、その設計に戻した。
Android Wearからスマホ本体側にメッセージを送る

0

Android WearのエミュレータをAndroid実機本体に接続する

Moto360へのapk書き込みが遅すぎるので、Wearはエミュレータで、本体はNexus5でやってみる事にした。


参考

Creating and Running a Wearable App | Android Developers
のSet up an Android Wear Virtual Device


Wearエミュレータの設定

Android Studioの[Tools]→[Android]→[AVD Manager]でWearのエミュレータを作る。ARMよりx86のイメージの方が速いらしい。

「About」からビルド番号を7回タップして開発者オプションを有効にし、ADB Debuggingを有効にする


この時点ではWearは親機とペアリングして無いので、Disconnectedになっている


Android実機の設定

Android実機がエミュレータと接続できるようにしておく。
AndroidStudioのTerminalで
% adb -d forward tcp:5601 tcp:5601

そして接続する。
Android実機でGoogle製のAndroid Wearアプリを起動し、普段使ってるMoto360へのペアリングを解除して、右上から「エミュレータをペア設定」で接続。


音声入力

マイクが無いのでキーボードで入力する。タイミングがすごい微妙で、
音声入力画面になる→2秒ぐらい待つ→「Didn’t catch that」と表示される→表示が消える→ここで素早く入力→enterとか押さずにしばらく待つ→するとなぜか入力できてる。

0

Android Wearからスマホ本体側にsendMessageでメッセージを送る

参考

Sending and Receiving Messages | Android Developers
これ読んであとは勘で書いたらWearable.MessageApi.sendMessageがなんとなく動いた。


Wearable.MessageApi.sendMessageの用途

Wearにはスマホ側の通知が全部表示されてそこからアクションも全部実行できるんだけど、スマホ通知からWearのActivityを起動したりはできない。スマホからWearのActivityとか呼びたい時は、sendMessageでWear内のWearableListenerServiceを実装したサービスと通信して、そこからWear内でIntent発行してもらってActivityを起こす。

Wear上のActivityからスマホに通信して何か処理してもらう時もsendMessageを使う。
WearにはWebブラウザが無いので、twitterクライアント作る時とか少なくとも認証はスマホにやってもらう必要がある。

sendMessageとは別にDataItemというWear/Handheld間の共有オブジェクトもあって、これは変更通知もくるのでこっちでやった方がいい用途もある


WearからHandheld(スマホ)へ送る場合の手順

  1. WearでGoogle API Client接続
  2. Wearable.NodeApiでHandheldのNodeIDを探す
  3. sendMessageにpath(と呼ばれてるけどタイトルみたいなもの)とdataを送信
  4. Handheld側でWearableListenerServiceを作っておけば受信できる


Wear側でGoogle API Clientを作って、接続しておく

onStartで接続してonStopで切るとかは、AndroidStudioで新規プロジェクト作成時のMobileのMainActivityをGoogle Play Service Activityにすると生成されるテンプレを参考にした。
メッセージ受診するだけのHandheld側にはGoogle API Clientを作る必要はない。


Node ID

Wearable.NodeApi.getConnectedNodesでHandheldのnodeIDを取得する。Nodeを判別できる要素はgetDisplayNameしかなくて親機を正確に判別できる気がしないし、ふつうHandheldとWearしか存在してないはずなので全員に送信するようにしてしまった。


WearからsendMessage

UIスレッドから呼べないのでAsyncTaskの中でやるとかする。

    public void sendTweetAsync(String tweet){
if(tweet == null) return;
Log.i(TAG, "send \""+tweet+"\" to handheld");
new AsyncTask<String, Void, String>() {
@Override
protected String doInBackground(String... params) {
String tweet = params[0];
byte[] bytes;
try {
bytes = tweet.getBytes("UTF-8");
}
catch(Exception ex){
Log.e(TAG, ex.getMessage());
return null;
}
for (Node node : Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await().getNodes()){
Log.v(TAG, "sending to node:" + node.getId());
Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), "/post/tweet", bytes)
.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult sendMessageResult) {
Log.v(TAG, sendMessageResult.toString());
}
});
}
return null;
}
}.execute(tweet);
}


HandheldにWearableListenerServiceを作って受信


受信側はService作ってintent filterにwearable.BIND_LISTENERを追加するとライフサイクルは適当に管理してくれる。自分でGoogle API Clientの接続管理する必要もない。
        <service android:name=".TweetService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>

public class TweetService extends WearableListenerService {

@Override
public void onMessageReceived(MessageEvent messageEvent) {
Log.v(TAG, "onMessageReceived");
if (messageEvent.getPath().equals(MESSAGE_PATH_TWEET)) {
String msg;
try {
msg = new String(messageEvent.getData(), "UTF-8");
}
catch (Exception ex){
Log.e(TAG, ex.getMessage());
return;
}
Log.i(TAG, "receive: "+ msg);
// ここで適当に処理
}
}


Activityで受信する場合はMessageListenerを実装すればいいらしい。