Nearby Messages APIでチャットみたいなのを作ってみる

Google Play Services 7.8.0が[リリース](http://android-developers.blogspot.jp/2015/08/google-play-services-78-lets-see-whats.html)されました。
今回のバージョンアップの目玉はなんといってもNearbyでしょう。
このNearbyですが、近くの端末にメッセージを送れるNearby Messagesと近くの端末と接続を行うNearby Connectionsの2つの機能があります。
この記事では、Nearby Messagesを使って近くの端末がメッセージを送り合える簡単なチャットのようなものを作ってみます。

## Nearby APIの有効化

Nearby Messages APIを使うには、[Google Developers Console](https://console.developers.google.com/)でAPIを有効にし、アプリ用のクライアントキーを作成する必要があります。

### プロジェクトの作成

1. [Google Developers Console](https://console.developers.google.com/)にアクセス
2. `プロジェクトを作成`をクリック
3. プロジェクト名に適当な名前を入力してプロジェクトを作成

### Nearby Messages APIの有効化

1. [Google Developers Console](https://console.developers.google.com/)にアクセス
2. 作成したプロジェクトを選択
2. サイドメニューからAPIを選択
3. 検索窓に`Nearby Messages API`と入力して検索結果をクリック
4. `APIを有効にする`をクリック

### アプリ署名のSHA1を確認する

アプリの署名に使用しているkeystoreのSHA1を確認します。debug.keystoreでも構いません。
SHA1は以下のコマンドで確認できます。
確認したSHA1は次の手順で使用します。

```bash
keytool -exportcert -alias [使用するエイリアス] -keystore [使用するkeystore] -list -v
```

### クライアントキーを作成する

この手順で作成したクライアントキーはアプリ側で使用します。

1. [Google Developers Console](https://console.developers.google.com/)にアクセス
2. `APIと認証 > 認証情報`を選択
3. `新しいキーを作成`をクリック
4. `Android キー`をクリック
5. 入力欄に`前の手順で確認したSHA1;パッケージ名`の形式で入力
6. キーが作成されるのでAPIキーの項目をメモ

## Androidプロジェクトの準備

### Google Play Servicesの導入

Google Play ServicesのNearbyを`build.gradle`に追加

```groovy
compile 'com.google.android.gms:play-services-nearby:7.8.0'
```

### AndroidManifestに設定追加

`application`タグ内に`meta-data`を追加します。`android:value`には上の手順で作成したAPIキーを入力してください。

```xml
<application ...>
    <meta-data
        android:name="com.google.android.nearby.messages.API_KEY"
        android:value="作成したAPIキー" />
    <activity>
    ...
    </activit>
</application>
```

ここまででNearby Messages APIを使用するための準備が整いました。

## Nearby Messages APIを実装する

早速APIを使うための実装を行っていきます。
私の好みの問題でKotlinで実装していますがJavaでもやることは変わりません。

コードの全体はGithubに公開しているのでそちらを見てください。
[Github: chibatching/NearbySample](https://github.com/chibatching/NearbySample)

### Google API Clientの用意

APIに接続するためのクライアントを作成します。`callbacks`と`failedListener`については後ほど。

```kotlin
var mGoogleApiClient = GoogleApiClient.Builder(this)
        .addApi(Nearby.MESSAGES_API)
        .addConnectionCallbacks(callbacks)
        .addOnConnectionFailedListener(failedListener)
        .build()
```

次にクライアントからAPIに接続します。

```kotlin
if (!mGoogleApiClient.isConnected()) {
    mGoogleApiClient.connect()
}
```

ここで接続に成功した時に`addConnectionCallbacks`で登録したコールバックが、失敗した時には`addOnConnectionFailedListener`で登録したリスナーが呼ばれます。

### パーミッションの確認

Nearby Messages APIは実行時にユーザからの許可が必要です。
そのため、接続の成功時にNearby Messages APIを使用する許可があるか確認しています。
確認結果を受け取るコールバックが`GoogleApiClient.ConnectionCallbacks`を実装した`NearbyResultCallback`です。


```kotlin
// Nearby APIへの接続コールバック
private inner class NearbyConnectionCallbacks : GoogleApiClient.ConnectionCallbacks {
    override fun onConnected(connectionHint: Bundle?) {
        Log.d(TAG, "onConnected: $connectionHint")
        // 接続成功したらパーミッションを確認する
        Nearby.Messages.getPermissionStatus(googleApiClient)
                .setResultCallback(NearbyResultCallback("getPermissionStatus", {
                    // パーミッション取得済みの場合はメッセージのsubscribeを始める
                    Log.d(TAG, "Start subscribe")
                    Nearby.Messages.subscribe(googleApiClient, messageListener)
                }))
    }

    override fun onConnectionSuspended(p0: Int) {
    }
}

// Nearby Messages APIとの通信結果コールバック
private inner class NearbyResultCallback(
        val method: String, val runOnSuccess: () -> Unit) : ResultCallback<Status> {

    override fun onResult(status: Status) {
        if (status.isSuccess()) {
            // 通信結果が成功だったときには登録した成功時の処理を実行する
            Log.d(TAG, "$method succeeded.")
            this@MainActivity.runOnUiThread { runOnSuccess() }
        } else {
            // ユーザに許可を求めることができるか
            if (status.hasResolution()) {
                if (!mResolvingError) {
                    try {
                        // ユーザに許可を求めるダイアログを表示する
                        status.startResolutionForResult(this@MainActivity, REQUEST_RESOLVE_ERROR)
                        // publishとsubscribeのタイミングで2度呼ばれることがあるのでフラグで管理
                        mResolvingError = true
                    } catch (e: IntentSender.SendIntentException) {
                        Toast.makeText(this@MainActivity, "$method failed with exception: " + e, Toast.LENGTH_SHORT).show()
                    }
                } else {
                    // 二重で呼ばれたときにここに来る
                    Log.d(TAG, "$method failed with status: $status while resolving error.")
                }
            } else {
                Log.d(TAG, "$method failed with: $status resolving error: $mResolvingError")
            }
        }
    }
}
```

ユーザにパーミッションを要求するときはダイアログが表示されます。

![Screenshot_2015-08-16-16-12-31.png](https://qiita-image-store.s3.amazonaws.com/0/53706/e221cfd5-9226-f07e-be67-3fcc8196ef3b.png "Screenshot_2015-08-16-16-12-31.png")


このダイアログでユーザが許可したかどうか、`onActivityResult`で取得します。

```kotlin
// Nearbyのパーミッションダイアログの結果を受け取る
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_RESOLVE_ERROR) {
        mResolvingError = false
        if (resultCode == Activity.RESULT_OK) {
            // パーミッションを取得したらsubscribeを始める
            Log.d(TAG, "Start subscribe")
            Nearby.Messages.subscribe(googleApiClient, messageListener)
        } else {
            Log.d(TAG, "Failed to resolve error with code $resultCode")
        }
    }
}
```

### メッセージの送受信

ここまで来れば送信、受信どちらも難しくありません。

#### 送信

チャットぽいものを作るので、送信ボタンを押したらEditTextの内容を送ります。
メッセージのコンテンツはバイト配列形式にする必要があります。
`Message.MAX_CONTENT_SIZE_BYTES`を見ると102400バイトまで送れるようなのでメタデータと一緒にJSONにして送信しています。

```kotlin
// 送信ボタンを押したらEditTextの内容を送信する
sendButton.setOnClickListener {
    // メッセージデータのモデルを生成する
    val chatMessage = ChatMessage(editText.getText().toString(), System.currentTimeMillis(), true)
    // EditTextのクリアとhintにメッセージ表示
    editText.setText("")
    textInputLayout.setHint("Sending...")
    if (getCurrentFocus() != null) {
        // IMEを隠す
        val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0)
    }
    // Publish
    Nearby.Messages.publish(googleApiClient, Message(chatMessage.toString().toByteArray("UTF-8")), strategy)
            .setResultCallback(NearbyResultCallback("send", {
                // When send succeeded, show my message
                textInputLayout.setHint("Input message")
                addNewMessage(chatMessage)
            }))
}
```

```kotlin:ChatMessage
// メッセージデータのモデル
public data class ChatMessage(
        val text: String,
        val timestamp: Long,
        val self: Boolean,
        val id: String = UUID.randomUUID().toString()) : Comparable<ChatMessage> {

    companion object {
        // JSONからメッセージデータを生成
        fun fromJson(jsonString: String) : ChatMessage {
            val json = JSONObject(jsonString)
            return ChatMessage(json.getString("text"), json.getLong("timestamp"), json.getBoolean("self"), json.getString("id"))
        }
    }

    // メッセージデータをJSONに変換
    override fun toString(): String {
        val json = JSONObject()
        json.put("id", id)
        json.put("text", text)
        json.put("timestamp", timestamp)
        json.put("self", self)
        return json.toString()
    }

    ...
}
```

#### 受信

受信はいままでのコードにもちょくちょく出てきますが、以下のコードで待機を開始します。
メッセージが届けばリスナーの処理を実行します。今回はメッセージをリストに追加していきます。

```kotlin
Nearby.Messages.subscribe(googleApiClient, messageListener)
```

```kotlin
val messageListener = object : MessageListener() {
    override fun onFound(message: Message?) {
        Log.d(TAG, "onFound: ${message?.toString()}")
        if (message != null) {
            addNewMessage(ChatMessage.fromJson(String(message.getContent(), "UTF-8")))
        }
    }
}

// メッセージをリストに追加してタイムスタンプ順に並び替え
private fun addNewMessage(message: ChatMessage) {
    if (!messageList.contains(message)) {
        messageList.add(message)
        Collections.sort(messageList)
        this@MainActivity.runOnUiThread {
            messageAdapter.notifyDataSetChanged()
        }
    }
}
```

### 接続解除

バッテリー持ちに大きく影響するので、アプリがバックグラウンドになった際や必要無くなったときには接続を解除します。

```kotlin
override fun onStart() {
    super.onStart()
    if (!googleApiClient.isConnected()) {
        // バックグラウンドから復帰したら接続し直す
        googleApiClient.connect()
    }
}

override fun onStop() {
    if (googleApiClient.isConnected()) {
        Nearby.Messages.unsubscribe(googleApiClient, messageListener)
        // 本当はpublishもunpublishしたほうがいい
    }
    googleApiClient.disconnect()
    super.onStop()
}
```

### 注意点

**(追記)** 試していて軽くはまったところをメモ

1. Google API ClientでNearbyに接続した際、publishされていてTTLの時間内(デフォルト300秒)のものがまとめて受信される
    - アプリをバックグラウンドから復帰して再接続した時に重複チェックが必要
    - TTLはpublish時に渡すstrategyで変更可能
2. メッセージをunpublishすると、まだ受信していない端末にはメッセージが届かなくなる
    - 今回のサンプルでunpublishをしていない理由
    - TTLが過ぎて自然消滅するのを待つ

## 動かしてみる

複数の端末にアプリをインストール、すべての端末でNearbyの許可を出したら準備完了です。
一つの端末でメッセージを送信すると他のすべての端末でメッセージが表示されるはずです！

![Screenshot_2015-08-16-17-05-19.png](https://qiita-image-store.s3.amazonaws.com/0/53706/266e1cfb-b541-a794-ba73-69dcb84e8593.png)

## おわり

端末をAPIに接続して、ユーザからNearbyの使用許可を取得する部分は若干手間ですが、メッセージの送受信は非常に簡単に扱うことが出来ます。
メッセージも102400バイトと意外と大きなサイズが送れるみたいなので色々な使い方ができそうですね。

コード全体はこちら：[Github](https://github.com/chibatching/NearbySample/tree/master)