android wear勉強会2

Post on 08-Sep-2014

7 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

DESCRIPTION

android were勉強会#2 東京で行われました。

TRANSCRIPT

僕がAndroidWearアプリを何も考えずに作った結果

AndroidWear勉強会#2 夜子まま

ストップウォッチが 出来るまで

項目• 作ったきっかけとWearアプリへの落とし込み

• AndroidWearアプリの実装で躓いた点

• データ共有処理

• AndroidWearアプリで出来る事

• まとめ

もともとはバスケの練習用

練習メニューの作成

ストップウォッチ、タイマー による計測

日々の記録とグラフ化

今までは 標準のタイマーとEVERNOTEを併用

フォーマットは自由にできるので、とりあえず記録しておいて

いつかPCでなんとかしてやれ感

WorkOutアプリ練習メニューを確認

メニューに応じて、タイマー、ストップウォッチ、回数表示の

機能に

アプリ設計

DB

WorkOut WorkLog

DB

データ通知

WorkOutItem

Wear側の設計ワーク名

タイマー

前回のタイム/最高記録

開始、停止ボタン

次のワークへ

ここで問題に気づく

この仕様だと、設定された順番でしか ワークをこなせない、何らかの方法で

選択をしなければ使いにくい

方法1:スピナー使えた

だけど画面が狭い

方法2:ダイアログ

Sel

使えた だけど画面がごちゃご

ちゃしすぎ

ここでふと別の問題に気づいた

うち、双子だった

この画面で二人分の記録を同時に出来るようにするには?

二人以上の記録

• 記録にタグ付けをする

• Lap機能を追加し、数人の記録ができるようにする

うーんっっ

やめることにした

そもそもWearableは 一人で使うもの

1:1 1:N

こっち

ストップウォッチに特化 してみることにした

ストップウォッチ設計

DB

RecordTag RecordLog

DB

タグの追加

記録にタグ付け

ストップウォッチ設計

DB

RecordTag RecordLog

DB

記録のグラフ表示

外部アプリとの連携

ストップウォッチ設計

必要最低限の機能にとどめて、タグ付けや、削除といった作業はスマホ側

で行う。

スマホとウォッチ間の 情報共有

データ共有

Message Data Layer

Message

指定されたpathにデータを送信する。 送信時に接続がなくても処理が終了するため、

一方通行なデータ送信に適している。

送信実装例 DataMap dataMap = new DataMap(); dataMap.putLong(KEY_START_TIME, startTime); Collection<String> nodes = getNodes(); for (String node : nodes) { Wearable.MessageApi.sendMessage( mGoogleApiClient, node, START_TIMER_PATH, dataMap.toByteArray()).setResultCallback( new ResultCallback<MessageApi.SendMessageResult>() { @Override public void onResult(MessageApi.SendMessageResult sendMessageResult) { if (!sendMessageResult.getStatus().isSuccess()) { Log.e(TAG, "Failed to send message with status code: " + sendMessageResult.getStatus().getStatusCode()); } } } ); }

DataMapクラスを使うとbyte[]化が楽

OnResultというけど、送信の可否程度で、結果を受信側で作成する仕組みはなさそう。

リスナー実装例

@Override public void onMessageReceived(MessageEvent messageEvent) { Log.d(TAG, "onMessageReceived: " + messageEvent); }

MessageApi.MessageListenerを implementsし 下記のメソッドをOverrideする

Wearable.MessageApi.addListener(mGoogleApiClient, this);

実装したリスナーの登録

実装したリスナーの解除

Wearable.MessageApi.removeListener(mGoogleApiClient, this);

onMessageReceiveの実装例

@Override public void onMessageReceived(MessageEvent messageEvent) { Log.d(TAG, "onMessageReceived: " + messageEvent); ! // Check to see if the message is to start an activity if (messageEvent.getPath().equals(START_ACTIVITY_PATH)) { Intent startIntent = new Intent(this, MainWearActivity.class); startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(startIntent); } }

MessageEventに登録したPathで判断するのがいいみたい。

僕がミスった例リスナーの解除しわすれをしていたため、同じMessage

が何度も飛んできた。 アプリを再起動しても解除されないので???な状況に

07-11 10:54:57.962 5509-5672/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.962 5509-5610/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.962 5509-5660/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.962 5509-5521/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.962 5509-5539/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5570/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5604/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5601/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5566/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5520/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5582/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.972 5509-5567/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:57.982 5509-5587/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:58.002 5509-5581/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:58.052 5509-5581/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:58.052 5509-5587/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication! 07-11 10:54:58.062 5509-5570/com.wearTest.message D/DEVELOPER﹕ ......Wear: successfully received phone to wear communication!

http://stackoverflow.com/questions/24702402/android-mobile-wear-app-onmessagereceived-called-multiple-times-for-one-message

Stackoverflowにも事例あり

DataLayer

DataItemやAssetsを共有データとしてStoreに登録する。 その後、変更通知を受け取るよう設定されたデバイスは

変更されたデータを取得することができる。 接続されていない場合は一旦待機し、接続された際に

変更を通知するので、同期的なデータ共有に適している。

store

登録実装例PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH); putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++); PutDataRequest request = putDataMapRequest.asPutDataRequest(); !if (!mGoogleApiClient.isConnected()) { return; } Wearable.DataApi.putDataItem(mGoogleApiClient, request) .setResultCallback(new ResultCallback<DataItemResult>() { @Override public void onResult(DataItemResult dataItemResult) { if (!dataItemResult.getStatus().isSuccess()) { Log.e(TAG, "ERROR: failed to putDataItem, status code: " + dataItemResult.getStatus().getStatusCode()); } } });

登録用のデータを作成 Uriは内部で自動生成

一個ずつしか登録できないみたい

取得実装例PendingResult<DataItemBuffer> results = Wearable.DataApi.getDataItems(mGoogleApiClient); results.setResultCallback(new ResultCallback<DataItemBuffer>() { @Override public void onResult(DataItemBuffer dataItems) { if (dataItems.getCount() != 0) { DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItems.get(0)); ! // This should read the correct value. int value = dataMapItem.getDataMap().getInt(COUNT_KEY); } dataItems.release(); } });

登録したUriを指定、あるいは全部取得の二択

登録数分配列で帰ってくるので、目的のものを検査して処理す

るみたい

リスナー設定Activityの場合

DataListenerの実装だけでよい

Serviceの場合WearableListenerServieを実施、且つ AndroidManifestに一つFilterを追加する必要あり

リスナーの実装例(Activity)

@Override public abstract void onDataChanged (DataEventBuffer dataEvents) { Log.d(TAG, "onDataChanged: " + dataEvents); }

Activityの場合はDataApi.DataListenerを implementsし 下記のメソッドをOverrideする

Wearable.DataApi.addListener(mGoogleApiClient, this);

Activityの場合はさらに、実装したリスナーの登録も必要

登録したリスナーの解除は必ず行うようにすること Wearable.DataApi.removeListener(mGoogleApiClient, this);

リスナーの実装例(Service)

@Override public abstract void onDataChanged (DataEventBuffer dataEvents) { Log.d(TAG, "onDataChanged: " + dataEvents); }

Serviceの場合はWearableListenerServiceを extendsし 下記のメソッドをOverrideする

<service android:name=".HomeListenerService"> <intent-filter> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> </intent-filter> </service>

AndroidManifest.xmlのServiecの定義に下記のFilterも追加する

onDataChangedの実装例 @Override public void onDataChanged(DataEventBuffer dataEvents) { final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); for (DataEvent event : events) { String path = event.getDataItem().getUri().getPath(); if (event.getType() == DataEvent.TYPE_CHANGED) { Log.d(TAG, "request path:" + path); } else if (event.getType() == DataEvent.TYPE_DELETED) { } else { } } }

データの更新種別によって処理を振り分ける。 Createでないのがなんとなくしっくりこない。

Uriの生成private Uri getUriForDataItem() { // ローカルのノード上のデータのUriの場合 String nodeId = getLocalNodeId(); // リモートの場合は getRemoteNodeId()を使う、またはすでに固定で知っているなら直接打ち込んでもいい return new Uri.Builder().scheme(PutDataRequest.WEAR_URI_SCHEME).authority(nodeId).path("/path_to_data").build(); } !private String getLocalNodeId() { NodeApi.GetLocalNodeResult nodeResult = Wearable.NodeApi.getLocalNode(mGoogleApiClient).await(); return nodeResult.getNode().getId(); } !private String getRemoteNodeId() { HashSet<String> results = new HashSet<String>(); NodeApi.GetConnectedNodesResult nodesResult = Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); List<Node> nodes = nodesResult.getNodes(); if (nodes.size() > 0) { return nodes.get(0).getId(); } return null }

StackOverflowをみてたらいいのを見つけた。

http://stackoverflow.com/questions/24601251/what-is-the-uri-for-wearable-dataapi-getdataitem-after-using-putdatamaprequest

僕がミスった例

07-15 19:16:10.185 9549-9569/? D/MainActivity﹕ onDataChanged: com.google.android.gms.wearable.DataEventBuffer@191106b8 07-15 19:16:10.186 9549-9569/? D/MainActivity﹕ request path:/action-add 07-15 19:16:10.203 9549-9569/? W/Binder﹕ Caught a RuntimeException from the binder stub implementation. java.lang.SecurityException: Permission Denial: reading com.kayosystem.android.easystopwatch.RecordContentProvider uri content://com.kayosystem.android.easystopwatch/tb_recordlog from pid=0, uid=10009 requires the provider be exported, or grantUriPermission() at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:498) at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:425) at android.content.ContentProvider$Transport.query(ContentProvider.java:195) at android.content.ContentResolver.query(ContentResolver.java:466) at android.content.ContentResolver.query(ContentResolver.java:410) at com.kayosystem.android.easystopwatch.MainActivity.registerdCheck(MainActivity.java:326) at com.kayosystem.android.easystopwatch.MainActivity.onDataChanged(MainActivity.java:211) at com.google.android.gms.wearable.internal.av.d(Unknown Source) at com.google.android.gms.wearable.internal.ac$a.onTransact(Unknown Source) at android.os.Binder.execTransact(Binder.java:404) 07

Wear側からDataLayerを使って変更を通知し、Mobile側でContentProviderでデータ・アクセスしたら落ちた。

OnDataChangeはWearのプロセスIDからの実行になっている。 処理の前に

long token =Binder.clearCallingIdentity()をよび、処理がおわったら、Binder.restoreCallingIdentity(token)を呼ぶ。

あるいは、runOnUiThreadでも回避できた。

おまけ

幾つかやってて 気づいたこと

Activityの遷移は出来る?

出来ました

Fragmentの遷移は出来る?

出来ました

Dialogは使える?

出来ました

SQLiteは使える?

出来ました

Environment File data = Environment.getDataDirectory(); File external = Environment.getExternalStorageDirectory(); File download = Environment.getDownloadCacheDirectory(); File root = Environment.getRootDirectory(); ! Log.d(TAG, "data:" + data.getAbsolutePath()); Log.d(TAG, "external:" + external.getAbsolutePath()); Log.d(TAG, "download:" + download.getAbsolutePath()); Log.d(TAG, "root:" + root.getAbsolutePath());

07-14 11:09:50.600 14757-14757/com.kayosystem.android.wearsample D/Fragment1﹕ data:/data 07-14 11:09:50.610 14757-14757/com.kayosystem.android.wearsample D/Fragment1﹕ external:/storage/emulated/0 07-14 11:09:50.610 14757-14757/com.kayosystem.android.wearsample D/Fragment1﹕ download:/cache 07-14 11:09:50.610 14757-14757/com.kayosystem.android.wearsample D/Fragment1﹕ root:/system

SDCardない

“/system/etc/vold.fstab", “/etc/vold.fstab" はともに読み取りできませんでした。

URLConnectionは使えるか?

使えない

07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ java.io.EOFException 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.Util.readAsciiLine(Util.java:342) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.RawHeaders.fromBytes(RawHeaders.java:311) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.HttpTransport.readResponseHeaders(HttpTransport.java:135) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:644) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:353) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:297) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.android.okhttp.internal.http.HttpURLConnectionImpl.getHeaderFields(HttpURLConnectionImpl.java:161) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.kayosystem.android.wearsample.Fragment1.getTechbooster(Fragment1.java:242) 07-14 11:42:34.495 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.kayosystem.android.wearsample.Fragment1.access$300(Fragment1.java:36) 07-14 11:42:34.505 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at com.kayosystem.android.wearsample.Fragment1$5.run(Fragment1.java:218) 07-14 11:42:34.505 1208-1236/com.kayosystem.android.wearsample W/System.err﹕ at java.lang.Thread.run(Thread.java:841)

ペアリングする前だと、UnknownHostでエラーが発生 ペアリング後だと、Connectionは成功したけど、そのあと落ちた

PackageManagerのhasFeatureの結果(LGG)

android.software.app_widgets not supported. android.hardware.audio.low_latency not supported. android.software.backup not supported. android.hardware.bluetooth supported. android.hardware.bluetooth_le supported. android.hardware.camera not supported. android.hardware.camera.any not supported. android.hardware.camera.autofocus not supported. android.hardware.camera.external not supported.

PackageManagerのhasFeatureの結果(LGG)

android.hardware.camera.flash not supported. android.hardware.camera.front not supported. android.hardware.consumerir not supported. android.software.device_admin not supported. android.hardware.faketouch supported. android.hardware.faketouch.multitouch.distinct not supported. android.hardware.faketouch.multitouch.jazzhand not supported. android.software.home_screen supported. android.software.input_methods not supported.

PackageManagerのhasFeatureの結果(LGG)

android.software.leanback not supported. android.software.leanback_only not supported. android.software.live_wallpaper not supported. android.hardware.location supported. android.hardware.location.gps not supported. android.hardware.location.network not supported. android.hardware.microphone supported. android.hardware.nfc not supported. android.hardware.nfc.hce not supported. android.hardware.nfc.hce not supported.

PackageManagerのhasFeatureの結果(LGG)

android.software.print not supported. android.hardware.screen.landscape not supported. android.hardware.screen.portrait supported. android.hardware.sensor.accelerometer supported. android.hardware.sensor.barometer not supported. android.hardware.sensor.compass supported. android.hardware.sensor.gyroscope supported. android.hardware.sensor.heartrate not supported. android.hardware.sensor.light not supported.

PackageManagerのhasFeatureの結果(LGG)

android.hardware.sensor.proximity not supported. android.hardware.sensor.stepcounter supported. android.hardware.sensor.stepdetector supported. android.software.sip not supported. android.software.sip.voip not supported. android.hardware.telephony not supported. android.hardware.telephony.cdma not supported. android.hardware.telephony.gsm not supported. android.hardware.type.television not supported. android.hardware.touchscreen supported.

PackageManagerのhasFeatureの結果(LGG)

android.hardware.touchscreen.multitouch supported. android.hardware.touchscreen.multitouch.distinct not supported. android.hardware.touchscreen.multitouch.jazzhand not supported. android.hardware.usb.accessory supported. android.hardware.usb.host not supported. android.hardware.type.watch supported. android.software.webview not supported. android.hardware.wifi not supported. android.hardware.wifi.direct not supported.

へぇー

やってみた感想

思っていた以上になんでもできた。 僕はてっきり、Mobileのサブ画面的な位置付け

で考えていたので、データの同期は高速で、またほとんどの処理をモバイル側で暗黙的に処理されるような仕組みだ

と勝手に想像していた。 だけど実際やっぱり別デバイスなんだなと痛感させられた

だから理解に苦しんだところがあった。 しかし良く考えると、これはやはりAndorid端末なので、 なんでもやれるし、面白いデバイスだと思いました。

top related