LoginSignup
20
21

More than 5 years have passed since last update.

AndroidでP2PやるならNearbyを使おう

Last updated at Posted at 2018-12-12

Mohikanz Advent Calendar 2018 #1 12日目の記事です。
前日は @MakTak さんの Node.jsでiBeaconの距離推定する でした。

TL;DR

Wi-Fi peer-to-peerもあるけど、Nearbyも良いよ!

端末同士でデータを散発的に送り合うようなものを作りたくて、調べたあれこれになります。

Wi-Fi peer-to-peer

Androidでpeer-to-peer(=P2P)、端末間通信をする場合、まず候補に上がるのが標準機能に含まれるWi-Fi peer-to-peerではないかと思います。
スクリーンショット 2018-12-12 22.38.37.png
公式のリファレンスどおり実装していけば問題なく通信確立でき、簡単なデータの送受信もできたので何も問題なさそうだったのですが…

ラグがひどい

一度に送るデータはせいぜい十数バイトだったのですがラグがすごかったのです…
早い時は数十〜数百msecで届くのが、数秒以上かかるケースが散発してしまい(数回に1度くらいの頻度)わりとリアルタイム性が必要だった今回のアプリではちょっと採用は厳しい感じでした。。
両方或いは片方の端末をネットワークから外したり、同じネットワークにつないだ状態にしたりと試してみましたが結果は変わらずでした。

Nearby

途方に暮れていたのですが、困った時の日本Androidの会
Nearbyというのがありまして、結論から言うとほとんどラグもなく双方向通信を行うことができました。
Nearby自体は近距離にあるデバイスの検知、通信を行うことで色々なサービスを作れるプラットフォームのようで、今回はその中のNearby Connections APIを利用していきます。

実装編

ここからはほとんど公式のパクリとおりですが実装していきます。
事前にGoogle Play Services SDKを開発環境に組み込んでおき、動かす端末にはGoogle Play開発者サービスの7.8以降がインストールされている必要があります。

まずはおなじみのパーミッション。

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

ご存知ACCESS_COARSE_LOCATIONは厄介なやつで、Android 6.0(APIレベル23)以降の端末では アプリの設定画面で許可しないとNearby自体が動きません
その他にも配布するようなアプリの場合は細かいお作法があります。詳しくはこちら

次にgradleの設定。今の最新版は11.8.0でした。

dependencies {
  implementation 'com.google.android.gms:play-services-nearby:{latest_version}'
}

Strategy

ここからはソースコードを書いていきます。
まずはStrategyとやらを選びます。
P2P_STAR(1対N型)とP2P_CLUSTER(M対N型)が選べますが、今回は接続してる端末それぞれからデータを投げ合いたかったのでP2P_CLUSTERを選択しました。

Advertise and discover

私ここよーとお知らせするadvertiseと、探しにいくdiscoverを開始します。両方同時に行うこともできます(後述のちょっとしたコツは要りました)。
advertiseはこんな感じ。

Nearby.getConnectionsClient(context).startAdvertising(
        "ユーザ名とか"
        context.packageName,
        connectionLifecycleCallback,
        AdvertisingOptions.Builder().setStrategy(Strategy.P2P_CLUSTER).build()
)
        .addOnSuccessListener { /* advertise開始成功 */ }
        .addOnFailureListener { /* advertise開始失敗 */ }

Strategy.P2P_CLUSTERをセットしています。connectionLifecycleCallbackについては後ほど。

discoverはこちら。

Nearby.getConnectionsClient(context).startDiscovery(
        context.packageName,
        endpointDiscoveryCallback,
        DiscoveryOptions.Builder().setStrategy(Strategy.P2P_CLUSTER).build()
)
        .addOnSuccessListener { /* discover開始成功 */ }
        .addOnFailureListener { /* discover開始失敗 */ }

endpointDiscoveryCallbackについても後ほど。

Manage connections

まずはendpointDiscoveryCallbackの実装をば。
段々恥ずかしい感じになってきておりますが…

private val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
    override fun onEndpointFound(endpointId: String, discoveredEndpointInfo: DiscoveredEndpointInfo) {
        val client = Nearby.getConnectionsClient(context)
        // ここでstopしないと接続できず
        client.stopAdvertising()
        client.stopDiscovery()
        GlobalScope.launch {
            // delay噛ませないとrequestConnectionに失敗する
            delay(1000)
            client
                    .requestConnection(
                            "ユーザ名とか",
                            endpointId,
                            connectionLifecycleCallback
                    )
                    .addOnFailureListener {
                        // リトライさせたり
                    }
        }
    }

    override fun onEndpointLost(endpointId: String) {
        // A previously discovered endpoint has gone away.
    }
}

コメントにあるとおり、自分の持ってる端末だけかもですが(Nexus 5と5Xで動かしてます)細かい調整をしてます。もっと良い方法があれば教えてください…!

前述のコツになるのですが、requestConnection()する前にstopAdvertising()とstopDiscovery()してます。
advertiseとdiscoverを同時にかけてるとここで止めないとうまくつながらず…どちらかの端末をadvertise、反対側をdiscoverで固定してればstopしなくてOKでした。

Accept or reject a connection

connectionLifecycleCallbackの実装に進みます。こやつはadvertiseした時と、endpointDiscoveryCallbackの中で接続要求にいく時にも渡しています。

private val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
    override fun onConnectionInitiated(endpointId: String, connectionInfo: ConnectionInfo) {
        // requestConnection()した方かされた方かはここで取れる
        val isDiscoverer = !connectionInfo.isIncomingConnection
        Nearby.getConnectionsClient(context).acceptConnection(endpointId, payloadCallback)
    }

    override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
        when (result.status.statusCode) {
            ConnectionsStatusCodes.STATUS_OK -> {
                // データのやり取りにendpointIdが必要なので保持る
                heldEndpointId = endpointId
            }
            ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED -> {
                // The connection was rejected by one or both sides.
            }
            ConnectionsStatusCodes.STATUS_ERROR -> {
                // The connection broke before it was able to be accepted.
            }
            else -> {
                // Unknown status code
            }
        }
    }

    override fun onDisconnected(endpointId: String) {
        // We've been disconnected from this endpoint. No more data can be
        // sent or received.
    }
}

セキュアなデータのやり取りをしたい場合はonConnectionInitiated()でゴニョゴニョしますが今回は割愛します。
payloadCallbackが出てきますが、ここまで来てようやくデータの送受信の準備ができました。

Exchange data

onConnectionResult()でSTATUS_OKが受け取れたらデータの送受信ができるようになります。
例えば文字列を送る場合はこんな感じで送ることができます。

Nearby
        .getConnectionsClient(context)
        .sendPayload(heldEndpointId, Payload.fromBytes(str.toByteArray()))

送れるペイロードの種類は現状ではBYTES/FILE/STREAMの3種類です。
今回は送るデータが少ないので手軽なBYTESを使いました。ちなみにBYTESでは32kbまでのデータを送れるようです。

受信側のコードはこんな感じになります。

private val payloadCallback = object : PayloadCallback() {
    override fun onPayloadReceived(endpointId: String, payload: Payload) {
        val b = payload.asBytes() ?: return
        val data = String(b)
    }

    override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
        when (update.status) {
            IN_PROGRESS -> {}
            SUCCESS -> {}
            FAILURE -> {}
            CANCELED -> {}
        }
    }
}

onPayloadTransferUpdate()でデータの受信状況をモニターできますが、

Unlike FILE and STREAM payloads, BYTES payloads are sent as a single chunk, so there is no need to wait for the SUCCESS update (although it will still be delivered, immediately after the call to onPayloadReceived())

公式にあるとおりBYTESペイロードの場合は一塊で送られてくるので、特に気にせずonPayloadReceived()でデータを取り出して完了です。

まとめ

Nearbyはclosed sourceなようで実装までは追えなかったのですが、BLEやWi-Fi、音声を組み合わせて使っているようです。
なのでシチュエーションによってはハマらないケースもあると思いますが、P2Pで何か作る時にはぜひ使ってみてほしいプラットフォームです。

明日(というか今日)13日は@netakaさんの電子ペーパー名札をつくってみたです。

参考

androidとiosでMultipeer接続
Nearby Connections APIを使ってみる
Release Notes
google sample

20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21