はじめまして、普段は技術部でAndroidエンジニアをしている千葉(@chibatching)です。 今回はトクバイでビーコンを使った施策を行った際に活用したGoogle Beacon Platformについて紹介したいと思います。
背景と課題
ご存知の通りトクバイはリアル店舗とユーザをつなげるためのサービスを展開し、ユーザ価値を高めていくために様々な施策を行っています。 その中で、実際にどれくらいのユーザが来店しているかを調査するため店舗様に協力していただき検証を行いました。
この検証を行うためにはアプリでユーザが実店舗に来店したことを検知しログを取得する必要があります。 そして、その来店検知にはビーコンを使うことになりました。
しかし、このプロジェクトでは
- 対象となる店舗は実装開始・アプリリリース時点では未定で複数店舗で実施される可能性がある
- ある店舗での検証終了後、同じビーコン端末を別の店舗で利用する可能性がある
- 検証対象店舗数の増加に伴ってビーコン端末自体の数も増える可能性がある
といった要件があり、アプリ側の変更・リリース無しで柔軟にログを取得していく必要がありました。
また、本検証はAndroidアプリだけで行うことになったのですがAndroid特有の事情として次のようなものを考慮する必要があります。
- Androidのバージョンが進むに従ってアプリのバックグラウンド実行の制限が段々と厳しくなっている
- 仮にバックグラウンドで実行したとしても常時実行しているとユーザへのバッテリー消費などの影響が大きい
この記事ではこれらの要求に対応するために採用したGoogle Beacon Platform及びNearby APIについて解説していきます。
iBeaconについて
本施策では、ビーコンとしてiBeacon規格の端末を利用しています。 詳しい解説に入る前にiBeaconについて概要をサクッとおさらいします。
iBeaconはBluetooth Low Energy (BLE) を利用したビーコンの1種でAppleが商標を持っています。 詳細な仕様は割愛しますが、iBeacon端末からブロードキャスト(Advertise)されるデータの構造は次のようになっています。
UUID
iBeaconにおけるUUIDは端末の個体を識別するためのものではなく、ブロードキャストされているデータのサービスやアプリを特定するためのものです。
- 16byte
- ユニークな値だが組織単位など目的に応じて同じUUIDを複数のデバイスに割り当てることも可能
Major, Minor
Major, MinorはUUIDからさらに1段2段細分化されたIDです。
- 各2byteずつの符号なし整数値(0~65535)
- UUIDとは別に設定することが可能な識別子
- すべてのUUIDを同じものにしてサービス/アプリ等を特定、Major/Minorで店舗、設置場所を特定するというような使い方が可能
ログ設計
上記のようなiBeaconの仕様から来店ログの仕様を考えてみます。
- UUIDをアプリ及び施策の識別に利用する
- Majorで店舗を特定
- Minorで店舗内での設置場所を特定
これで良さそうな気がしてきますね?しかし要件を満たす上で少し問題があります。
同じビーコンを別店舗で利用する可能性がある
この要件があるため、ログを分析するためにあるビーコンを別の店舗に移設する場合に次のどちらかの対応が必要になります。
- 分析時に対象のログを店舗での計測実施期間毎に分ける
- 別店舗への移設前にMajor, Minorの変更作業を行う
1の方針を取った場合、分析時にMajor, Minor毎にどの期間どの店舗どの場所に設置されていたかのマッピング情報が必要になります。 これは不可能ではないですが、管理や分析が煩雑になりそうです。
2の方針を取った場合
- 別店舗での実施前に一度ビーコンを持ち帰り、設定変更する
- 変更権限のある人間が現地まで出向き、設定変更する
のいずれかの対応が必要になります。 設置箇所が近隣だけであればこちらの対応でも可能ですが、トクバイに情報を掲載していただいている店舗は全国にあるため常にこの対応を行うというのは少々厳しいと言えます。
Google Beacon Platform
そこで今回利用したのがGoogle Beacon Platformです。 Google Beacon PlatformではBeacon Dashboardで登録したビーコンをNearby Messages APIを用いて検知し、ビーコン毎に設定した情報の取得を行うことができます。
この機能を利用してBeacon Dashboardからビーコン毎に店舗ID、ビーコン識別ID等を含んだJSONをアプリに配信、ビーコン検知時にログサーバにそのまま送信します。
分析時にはTreasure Dataのjson_extract関数を使って展開することでそのまま分析することが可能になります。
ビーコンを別の店舗に移設する際にはダッシュボードからビーコンに紐づく情報を修正するだけでよく、作業や開発の手間はかなり抑えられることになります。
また、AndroidではGoogle Play Servicesが提供するNearby Messages APIを利用してシステムからアプリに通知が行われるためアプリを常にバックグラウンドで実行しておく必要がありません。
これはアプリのバッテリー影響を抑えることにも繋がります。
具体的な利用手順を次に示します。
Beacon Dashboardへの登録
まずはBeacon Dashboardにアクセスしてダッシュボードの利用を開始します。 GoogleアカウントにログインしていればGoogle Cloud Platformのプロジェクト選択画面が表示されるので、ビーコンを登録するプロジェクトを選択、無ければ作りましょう。
すると次のような画面が表示されます。
意外なのですがビーコン端末をこのダッシュボードから登録することはできません。 ビーコンを登録するにはGoogleが提供しているAndroidアプリやiOSアプリを利用するかREST APIで登録する必要があります。
トクバイで所持しているビーコン端末はアプリからの登録がうまくできずREST APIを使う必要がありましたが、認証周りが面倒だったので今回はGoogle API ExplorerからAPIを叩いて登録を行いました。
パラメータはいろいろあるのですが、必須項目は多くはなく上の画像のような内容だけで登録できます。
AdvertisedIdとして設定する部分の生成が少し難しいため次に解説します。
AdvertisedIdの生成
iBeaconにおけるAdvertisedIdはUUID(16byte)+Major(2byte)+Minor(2byte)の計20byteです。
UUIDは多くの場合Hexで書かれており32文字、Major/Minorは2byteの符号なし整数値なのでHexにすると 0000
~ FFFF
で各4文字になります。
APIで指定するAdvertisedIdには、byte値をbase64エンコーディングしたものを指定する必要があります。
しがたって、UUID 00000000BBBBBBBCCCCCCCCCAAAAAAAA
, Major 1
, Minor 1
のiBeaconを登録するには 00000000BBBBBBBCCCCCCCCCAAAAAAAA00010001
をバイト列に変換し、さらにbase64にエンコードすることになります。
この変換にはGoogleのサンプルプロジェクトで紹介されているワンライナーを利用します(Python 3で動かないので微修正しています)。
id='00000000BBBBBBBCCCCCCCCCAAAAAAAA00010001'; python -c "import binascii; import base64; print(base64.b64encode(binascii.unhexlify('$id')))"
ビーコンに紐づく情報の登録
APIでビーコンを登録したあとはダッシュボードから情報を編集できます。 Descriptionに個体を識別する管理番号などを入れておくと一覧からも判別しやすくなります。
Google Beacon Platoformでは、アプリに配信される情報はAttachmentと呼ばれています。このAttachmentはビーコン詳細画面の View details
-> attachments
から指定することができます。
アプリ側では、ここで設定したNamespaceとTypeを指定してビーコンの情報を取得します。
Androidアプリ側実装
次にAndroidアプリ側の実装です。 先述したとおり、Beacon Dashboardに登録したビーコンの検知、情報取得はGoogle Play ServicesのNearby Messagesを利用します。
APIキーの取得は事前に行っておいてください。
まずは依存関係に com.google.android.gms:play-services-nearby
を追加します。執筆時点での最新は 16.0.0
です。
implementation "com.google.android.gms:play-services-nearby:${Versions.playServicesNearby}"
次に AndroidManifest.xml
にメタデータを追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.co.tokubai.android.bargain.beacon" > <application> ... <meta-data android:name="com.google.android.nearby.messages.API_KEY" android:value="[取得したAPIキー]" /> </application> </manifest>
次にビーコンの検知を始めます。 Nearby APIでビーコンを検知するためには位置情報のパーミッションが必要です。 今回はすでに位置情報の許諾を取得済みのユーザのみを対象として実装しました。
fun subscribe() { if (ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return // 位置情報のパーミッションが無いときはビーコン検知を行わない } // 受け取るメッセージのNamespaceとTypeを指定 val filter = MessageFilter.Builder() .includeNamespacedType("tokubai-namespace", "shop-visit-measurement") .build() // ビーコンの検知だけを行いたいのでStrategy.BLE_ONLYを指定 val options = SubscribeOptions.Builder() .setStrategy(Strategy.BLE_ONLY) .setFilter(filter) .build() // Nearby Messages API利用時には通常オプトインダイアログが表示されるが // ACCESS_FINE_LOCATIONパーミッションがありBLEのみを利用するときはダイアログの表示を省略できる // https://developers.google.com/nearby/messages/android/user-consent?hl=ja#ble_only val messagesOption = MessagesOptions.Builder() .setPermissions(NearbyPermissions.BLE) .build() // ビーコン検知時にGoogle Play Servicesから起動されるPendingIntentを登録 Nearby.getMessagesClient(application, messagesOption) .subscribe( ShopVisitBeaconReceiver.createPendingIntent(application), options ) }
次に、Google Play Servicesがビーコンを検知した際に通知を受け取るクラスを実装します。
今回はログを貯めるだけなのでBroadcastReceiverを使っていますが、重たい処理を行う必要があるときはServiceなどの利用を検討したほうがいいでしょう。
class ShopVisitBeaconReceiver : BroadcastReceiver() { companion object { fun createPendingIntent(context: Context): PendingIntent { return PendingIntent.getBroadcast( context, 0, Intent(context, ShopVisitBeaconReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT ) } } private val timber: Timber.Tree get() = Timber.tag(this::class.java.simpleName) override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) Nearby.getMessagesClient(context).handleIntent(intent, object : MessageListener() { // ビーコン信号を検知したときの処理 override fun onFound(p0: Message) { super.onFound(p0) val attachment = String(p0.content) // 検知したビーコンに紐づくattachmentを取得 timber.d("onFound: namespace=${p0.namespace} type=${p0.type} content=$attachment") // attachmentをそのままログとして収集 BeaconLog.sendFoundShopVisitMeasurementBeaconLog(attachment) } // ビーコン信号がロストしたときの処理 override fun onLost(p0: Message) { super.onLost(p0) val attachment = String(p0.content) timber.d("onLost: namespace=${p0.namespace} type=${p0.type} content=$attachment") BeaconLog.sendLostShopVisitMeasurementBeaconLog(attachment) } }) } }
これでアプリをバックグラウンドで起動しておく必要なくビーコンを検知、ログを収集できるようになりました。
まとめと課題
この仕組を実装し、半年ほどの検証を行いましたが目論見通り柔軟な運用ができました。
また、Android Vitalsの指標を見てもアプリが過剰に電池を消耗している様子もなくこちらも目論見通りの低消費電力によるビーコン検知が実現できました。
しかし、ビーコン端末を登録するUIが存在せず新規のビーコン端末を登録する際にはエンジニアが対応する必要がある、という課題もありました。 ビーコン端末の追加は頻繁に発生するタスクではなかったため問題にはなりませんでしたが、もっと大規模でビーコン端末の追加が頻繁に発生するような施策を行う場合には管理画面の開発が必要になってくるかもしれません。
とはいえ、サーバ側での開発なしに効率的にビーコンを管理する仕組みを構築することができたのは大きな成果でした。 みなさんもビーコンを使う施策を実施する機会があればGoogle Beacon Platformの利用を検討してみてはいかがでしょうか?