2014年7月2日水曜日

Android Wear アプリ開発 その2

Sending and Syncing Data

*Android 4.3(API Level 18)以上のスマホ、最新の Google Play services、Android Wear デバイスもしくはエミュレータが必要

Google Play services の一部である Wearable Data Layer API は、スマホとWear間のコミュニケーションチャネルを提供します。このAPIはシステムが送信・同期できるデータオブジェクトと、data layerで重要なイベントをアプリに通知するリスナーから構成されています。


Data Items

DataItem はスマホとWear間で自動的に同期するデータストレージを提供します。


Messages

MessageApi クラスでは、Wearからスマホのメディアプレイヤーをコントロールしたり、スマホからWear上でIntentを開始したりするような、"fire-and-forget" コマンド用にデザインされたメッセージを送ることができます。システムは、スマホとWearが接続されているときにメッセージを送り、デバイスが接続されていないときエラーを送ります。Message は一方通行のリクエストや、request/response コミュニケーションモデルに適しています。


Asset

Asset オブジェクトは、画像のようなバイナリー blob を送るためのものです。Data Items に Asset を取り付けると、システムが自動的に転送の面倒を見ます。例えば、再送信を回避するために大きい Asset をキャッシュして Bluetooth の帯域を節約します。


WearableListenerService (for services)

WearableListenerService を継承することで、重要な data layer イベントを Service で検知することができます。システムは WearableListenerService のライフサイクルを管理します。Data Item や Message を送る必要があるとき Service にバインドし、処理が必要なくなると Service をアンバインドします。


DataListener (for foreground activities)

Activity で DataListener を実装することで、Activity が foreground にあるときに重要な data layer イベントを検知することができます。WearableListenerService の代わりにこれをつかうことで、ユーザーがアクティブにアプリを利用しているときだけ検知できます。

*これらの API はスマホと Wear 間の通信用にデザインされているため、デバイス間の通信のセットアップにはこれらのAPIだけを使います。例えば、通信チャネルを作るためにローレベルソケットを開くことはしないでください。



Accessing the Wearable Data Layer

data layer API を呼ぶには、GoogleApiClient のインスタンスを生成します。これが Google Play services API の主なエントリーポイントです。

GoogleApiClient はクライアントのインスタンスを簡単に生成できるビルダーを提供しています。GoogleApiClient のミニマルな見た目は次にようになります。

* この段階ではミニマルなクライアントで十分です。コールバックを実装したりエラーをハンドリングするなど、GoogleApiClient を生成するより詳しい情報は Accessing Google Play services APIs を参照してください。 GoogleApiClient mGoogleAppiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(new ConnectionCallbacks() { @Override public void onConnected(Bundle connectionHint) { Log.d(TAG, "onConnected: " + connectionHint); } @Override public void onConnectionSuspended(int cause) { Log.d(TAG, "onConnectionSuspended: " + cause); } }) .addOnConnectionFailedListener(new OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult result) { Log.d(TAG, "onConnectionFailed: " + result); } }) .addApi(Wearable.API) .build();



Syncing Data Items

システムが、スマホとWear間でデータを同期するのに使うデータインタフェースの定義が DataItem です。
DataItem は一般的に次のアイテムで構成されています。

- Payload : バイト配列。任意のデータをセットでき、自身でオブジェクトの serialization ・ deserialization が可能。サイズのリミットは 100KB。

- Path : / から開始する一意の文字列(例えば、"/path/to/data")

普通は DataItem を直接実装しません。代わりに、

1. PutDataRequest オブジェクトを作成し、アイテムを一意に識別する文字列パスを指定する。

2. setData() を呼んで payload をセットする。

3. DataApi.putDataItem() を呼んで、システムに Data Item の生成をリクエストする。

4. Data Item をリクエストすると、システムは Data Item インタフェースを適切に実装したオブジェクトを返す。

生バイトで setData() を使う代わりに、Bundle のような使いやすいインタフェースで Data Item を扱える Data Map を利用することを推奨します。


Sync Data with a Data Map

可能なら、DataMap クラスを使います。これは Data Item を Bundle のような形で利用できるようにしたものです。そのため、オブジェクト serialization と deserialization を行ってくれ、key-value ペアでデータを操作できます。

Data Map を使うには、

1. PutDataMapRequest オブジェクトを作成し、Data Item のパスをセットする。

*文字列パスは Data Item を一意に識別するもので、コネクションのどちら側からでもアクセスできる。パスは / から始まらなければならない。もし階層的なデータを使うなら、データ構造に一致するパススキーマを生成する。

2. PutDataMapRequest.getDataMap() を呼んで DataMap を取得する

3. put...() メソッド(例えば putString())を使って、必要な値を DataMap にセットする。

4. PutDataMapRequest.asPutDataRequest() を呼んで PutDataRequest オブジェクトを取得する。

5. DataApi.putDataItem() を呼んで、システムに DataItem の作成をリクエストする。

* スマホとWearが接続されていない場合、データはバッファリングされ、接続されたときに同期される。 // パスを指定して PutDataMapRequest を生成 PutDataMapRequest dataMap = PutDataMapRequest.create("/count"); // 必要な値を DataMap にセット dataMap.getDataMap().putInt(COUNT_KEY, count++); // PutDataRequest を取得 PutDataRequest request = dataMap.asPutDataRequest(); // DataItem の生成をリクエスト PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi .putDataItem(mGoogleApiClient, request);


Listen for Data Item Events

data layer connection の片側で Data Item が変更された場合、connection の反対側でそれを検知したいことがあるでしょう。Data Item イベントのリスナー(DataApi.DataListener)を実装することでそれができます。 例えば、データが変更されたときに特定のアクションを実行する典型的なコールバックは次のようになります。 @Override public void onDataChanged(DataEventBuffer dataEvents) { for (DataEvent event : dataEvents) { 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()); } } }



Transferring Assets

画像のような大きな blob データをBluetoothで転送する場合、Data Item に Asset を取り付けて、Data Item を replicated data store に入れます。

Asset は自動的にデータのキャッシュを処理し、再送信を防いで Bluetooth 帯域を節約します。一般的なパターンとしては、スマホアプリで画像をダウンロードし、Wear の画面サイズに適したサイズに縮小してから Wear に転送します。以下の例はこのパターンです。

*Data Item のサイズは 100KB に制限されていますが、Asset はそれを超えて必要な大きさにすることができます。ただし、大きい Asset の転送は多くのケースでユーザーエクスペリエンスに影響するため、大きい Asset を転送しても十分なパフォーマンスが得られるかアプリをテストしてください。


Transfer an Asset

Asset クラスの create...() メソッドを使って Asset を作成します。 private static Asset createAssetFromBitmap(Bitmap bitmap) { final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); return Asset.createFromBytes(byteStream.toByteArray()); } DataMap もしくは PutDataRequest の putAsset() メソッドで Data Item に Asset を取り付け、putDataItem() で data store に Data Item を入れます。

Using PutDataRequest Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image); Asset asset = createAssetFromBitmap(bitmap); // パスを指定して PutDataRequest を生成 PutDataRequest request = PutDataRequest.create("/image"); // Asset をセット request.putAsset("profileImage", asset); // DataItem の生成をリクエスト PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi .putDataItem(mGoogleApiClient, request);

Using PutDataMapRequest Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image); Asset asset = createAssetFromBitmap(bitmap); // パスを指定して PutDataMapRequest を生成 PutDataMapRequest dataMap = PutDataMapRequest.create("/image"); // Asset をセット dataMap.getDataMap().putAsset("profileImage", asset); // PutDataRequest を取得 PutDataRequest request = dataMap.asPutDataRequest(); // DataItem の生成をリクエスト PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi .putDataItem(mGoogleApiClient, request);


Receive assets

以下は、Asset の変更を検知するためのコールバックの実装方法と、Asset を取り出す方法の例です。 @Override public void onDataChanged(DataEventBuffer dataEvents) { final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); for (DataEvent event : events) { if (event.getType() == DataEvent.TYPE_CHANGED && event.getDataItem().getUri().getPath().equals("/image")) { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); Asset asset = dataMapItem.getDataMap().getAsset("profileImage"); Bitmap bitmap = loadBitmapFromAsset(asset); // Do something with the bitmap } } } public Bitmap loadBitmapFromAsset(Asset asset) { if (asset == null) { throw new IllegalArgumentException("Asset must be non-null"); } // convert asset into a file descriptor and block until it's ready InputStream assetInputStream = Wearable.DataApi.getFdForAsset( mGoogleApiClient, asset).await().getInputStream(); if (assetInputStream == null) { Log.w(TAG, "Requested an unknown Asset."); return null; } // decode the stream into a bitmap return BitmapFactory.decodeStream(assetInputStream); }



Sending and Receiving Messages

MessageApi を使って Message を送ります。Message には以下のアイテムを含めます。

- 任意の payload(optional)
- Message のアクションを一意に識別するパス

Data Item と違い、スマホとWearアプリで同期されません。Message は一方通行のコミュニケーションメカニズムであり、Wear で Activity を開始させるためにメッセージを送るような "fire-and-forget" タスクに向いています。 また、片側がメッセージを送り、反対側が処理をして返事のメッセージを送り返す request / response モデルにも向いています。


Send a Message

以下は、コネクションの反対側に Activity を開始させるメッセージを送る例です。ここでは呼び出しを同期で行っているため、メッセージが受け取られるかリクエストがタイムアウトするまでブロックされます。

* Google Play services の非同期および同期呼び出しについての詳細は Communicate with Google Play Services を参照してください。 public static final START_ACTIVITY_PATH = "/start/MainActivity"; ... // 同期呼び出し SendMessageResult result = Wearable.MessageApi .sendMessage(mGoogleApiClient, nodeId, START_ACTIVITY_PATH, null) .await(); if (!result.getStatus().isSuccess()) { Log.e(TAG, "ERROR: failed to send Message: " + result.getStatus()); } 以下は、メッセージを送ることができる接続先ノードの一覧を取得する方法です。 private Collection<String> getNodes() { HashSet<String>results = new HashSet(); NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi .getConnectedNodes(mGoogleApiClient) .await(); for (Node node : nodes.getNodes()) { results.add(node.getId()); } return results; }


Receiving a Message

受信したメッセージを検知するには、Message イベントのリスナーを実装します。
以下は、上記のメッセージを受け取る例です。 @Override public void onMessageReceived(MessageEvent messageEvent) { if (messageEvent.getPath().equals(START_ACTIVITY_PATH)) { Intent startIntent = new Intent(this, MainActivity.class); startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(startIntent); } }



Handling Data Layer Events

data layer で呼び出しを行うと、完了時に呼び出しのステータスを受け取ることができます。


Wait for the Status of Data Layer Calls

putDataItem() など、data layer API への呼び出しはときどき PendingResult を返していることに気づくと思います。PendingResult が生成された直後は、操作はバックグラウンドでキューイングされています。この後何もしなければ操作は静かに完了します。しかし、操作が完了した後に結果を使って何かしたいことがあるでしょう。PendingResult を使って、同期・非同期どちらでも結果の状態を待つことができます。


Asynchronously waiting

コードがUIスレッドで走っているなら、data layer API の呼び出しでブロックしてはいけません。PendingResult オブジェクトにコールバックを追加して、呼び出しを非同期で行います。 pendingResult.setResultCallback(new ResultCallback<DataItemResult>() { @Override public void onResult(final DataItemResult result) { if(result.getStatus().isSuccess()) { Log.d(TAG, "Data item set: " + result.getDataItem().getUri()); } } });


Synchronously waiting

コードがバックグラウンドサービスのスレッドで走っている場合(例えば WearableListenerService)、呼び出しがブロックしても問題ないでしょう。PendingResult の await() を呼ぶことで、リクエストが完了するまで Result オブジェクトが返ってくるのがブロックされます。 DataItemResult result = pendingResult.await(); if(result.getStatus().isSuccess()) { Log.d(TAG, "Data item set: " + result.getDataItem().getUri()); }


Listen for Data Layer Events

data layer はスマホとWear間でデータを同期したり送ったりするため、Data Item が生成されたり、Message が受け取られたり、Wearとスマホが接続されたりなどの重要なイベントを検知したいことがあるでしょう。

data layer のイベントを検知するには、2つの方法があります。

- WearableListenerService を継承したサービスを作る
- DataApi.DataListener を実装した Activity を作る


With a WearableListenerService

典型的にはWearとスマホアプリ両方でこのサービスのインスタンスを生成するでしょう。もし片方のアプリでイベントを検知する必要がないなら、そのアプリではサービスを実装する必要はありません。

例えば、Data Item オブジェクトを取得したりセットしたりするスマホアプリがあり、Wearアプリではそれらの変更を検知してUIをアップデートするとします。この場合、Wearでは Data Item を変更しないのでスマホアプリではイベントを検知する必要がありません。

WearableListenerService で次のイベントを検知できます。

- onDataChanged() : Data Item オブジェクトが生成、変更、削除されたときに呼ばれます。コネクションの片側でのイベントが発生すると、両側のコールバックいずれも呼ばれます。

- onMessageReceived() : コネクションの片側から Message が送られると、反対側でこのコールバックが呼ばれます。

- onPeerConnection(), onPeerDisconnected() : スマホとWearが接続されたり、接続が外れたりすると呼ばれます。片側で接続状態が変わると、両側のコールバックいずれも呼ばれます。

WearableListenerService を作成するには、

1. WearableListenerService を継承したクラスを用意する

2. onDataChanged() など、処理したいコールバックを Override する

3. システムがサービスにバインドできるように AndroidManifest.xml で intent filter を追加する
public class DataLayerListenerService extends WearableListenerService { private static final String TAG = "DataLayerSample"; private static final String START_ACTIVITY_PATH = "/start-activity"; private static final String DATA_ITEM_RECEIVED_PATH = "/data-item-received"; GoogleApiClient mGoogleApiClient; @Override public void onCreate() { super.onCreate(); mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .build(); mGoogleApiClient.connect(); } @Override public void onDataChanged(DataEventBuffer dataEvents) { Log.d(TAG, "onDataChanged: " + dataEvents); final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); if(!mGoogleApiClient.isConnected()) { ConnectionResult connectionResult = mGoogleApiClient .blockingConnect(30, TimeUnit.SECONDS); if (!connectionResult.isSuccess()) { Log.e(TAG, "DataLayerListenerService failed to connect to GoogleApiClient."); return; } } // Loop through the events and send a message to the node that created the data item. for (DataEvent event : events) { Uri uri = event.getDataItem().getUri(); // Get the node id from the host value of the URI String nodeId = uri.getHost(); // Set the data of the message to be the bytes of the URI. byte[] payload = uri.toString().getBytes(); // Send the RPC Wearable.MessageApi.sendMessage(mGoogleApiClient, nodeId, DATA_ITEM_RECEIVED_PATH, payload); } } } 対応する AndroidManifest の設定 <service android:name=".DataLayerListenerService"> <intent-filter> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> </intent-filter> </service>


Permissions within Data Layer Callbacks

data layer イベントをアプリに通知するために、Google Play services は用意した WearableListenerService にバインドし IPC を介してコールバックを呼び出します。これは、アプリのコールバックが呼び出したプロセスの権限を継承するという結果になります。

コールバック内で特権のある操作を実行しようとすると、セキュリティチェックで失敗します。なぜなら、コールバックはアプリのプロセスIDではなく、呼び出し側のプロセスIDで実行されるからです。

これを直すには、clearCallingIdentity() を呼びます。IPC 境界を超えたあと ID をリセットし、特権のある操作を実行し、その後 restoreCallingIdentity() で ID をリストアします。 long token = Binder.clearCallingIdentity(); try { performOperationRequiringPermissions(); } finally { Binder.restoreCallingIdentity(token); }


With a Listener Activity

アプリがユーザーに利用されている間だけ data layer イベントを処理する必要があり、長い間実行されるService がいらない場合、次のインタフェースを Activity で実装することでイベントを検知できます。

- DataApi.DataListener
- MessageApi.MessageListener
- NodeApi.NodeListener

スマホアプリで data layer イベントを検知する Activity を作るには、

1. 必要なインタフェースを実装する

2. onCreate(Bundle) で GoogleApiClient のインスタンスを生成する

3. onStart() で connect() を呼び、Google Play services に接続する

4. Google Play services との接続が確立されたら、システムは ConnectionCallbacks の onConnected() を呼びます。ここで DataApi.addListener(), MessageApi.addListener(), NodeApi.addListener() を呼んで Google Play services に data layer イベントを検知したいことを伝えます。

5. onStop() で DataApi.removeListener(), MessageApi.removeListener(), NodeApi.removeListener() を呼んで登録を解除します。

6. インタフェースに応じて onDataChanged(), onMessageReceived(), onPeerConnected(), onPeerDisconnected() などを実装します。

以下は、スマホアプリで DataApi.DataListener を実装した例です。 public class MainActivity extends Activity implements DataApi.DataListener, ConnectionCallbacks, OnConnectionFailedListener { private GoogleApiClient mGoogleApiClient; private boolean mResolvingError = false; @Override public void onCreate(Bundle b) { super.onCreate(b); setContentView(R.layout.main_activity); ... mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } @Override protected void onStart() { super.onStart(); if (!mResolvingError) { mGoogleApiClient.connect(); } } @Override protected void onStop() { if (!mResolvingError) { Wearable.DataApi.removeListener(mGoogleApiClient, this); mGoogleApiClient.disconnect(); } super.onStop(); } @Override public void onConnected(Bundle connectionHint) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Connected to Google Api Service"); } mResolvingError = false; Wearable.DataApi.addListener(mGoogleApiClient, this); } @Override public void onConnectionFailed(ConnectionResult result) { if (mResolvingError) { // Already attempting to resolve an error. return; } else if (result.hasResolution()) { try { mResolvingError = true; result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR); } catch (IntentSender.SendIntentException e) { // There was an error with the resolution intent. Try again. mGoogleApiClient.connect(); } } else { Log.e(TAG, "Connection to Google API client has failed"); mResolvingError = false; Wearable.DataApi.removeListener(mGoogleApiClient, this); } } @Override public void onDataChanged(DataEventBuffer dataEvents) { final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); for (DataEvent event : events) { 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()); } } } ... 以下は、Wearアプリで DataApi.DataListener を実装した例です。 public class MainActivity extends Activity implements DataApi.DataListener, ConnectionCallbacks, OnConnectionFailedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } @Override protected void onResume() { super.onResume(); mGoogleApiClient.connect(); } @Override protected void onPause() { super.onPause(); if (null != mGoogleApiClient && mGoogleApiClient.isConnected()) { Wearable.DataApi.removeListener(mGoogleApiClient, this); mGoogleApiClient.disconnect(); } } @Override public void onConnected(Bundle connectionHint) { Log.d(TAG, "Connected to Google Api Service"); Wearable.DataApi.addListener(mGoogleApiClient, this); } @Override public void onDataChanged(DataEventBuffer dataEvents) { final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); dataEvents.close(); for (DataEvent event : events) { 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()); } } } ...



0 件のコメント:

コメントを投稿