Android
Kotlin
iBeacon
ClassiDay 9

[Android] iBeaconをビコーン(受信)したい by Kotlin

More than 1 year has passed since last update.

Classiアドベントカレンダー9日目です。

先日、弊社デザイナーが、
デザイナーとアプリエンジニアが仲良く開発できるためのチートシートを作るで、
iOSとAndroidの違いを意識を意識して会話することを書いてくれましたが、
Androidエンジニアが困る案件の1つに、iBeacon があります。

iBeaconは、Appleの商標で、Appleに特化したものであり、
さらにはAndroid SDKのBLEの実装が不安定で、AndroidでBLEをとても扱いにくいです。

そこで、登場したAltBeacon/android-beacon-library
という神が与えてくれたようなライブラリを利用することで、そんな不安を(ある程度)払拭してくれます。
(それでも、iOSと同じ挙動にしてくれとか不安になる一言を平然とぶつけられますが・・・)

目新しいことではないですが、
今回は、このライブラリを利用して、AndroidでiBeaconを受信する最低限のActivityを、Kotlinで書きました。

最低限必要な設定・ソースコード

1.パーミッションを設定する

AndroidManifest.xml
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <!-- BLE対応端末専用 -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
    <!-- Android M より、位置情報の許可も必須 -->
    <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>

2. android-beacon-library を使用する

app/build.gradle
dependencies {
    implementation 'org.altbeacon:android-beacon-library:2+'
}

3. iBeaconのフォーマットを定数化

BeaconUtil.kt
object BeaconUtil {
    const val IBEACON_FORMAT = "m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"
}

4. iBeaconの受信Activity(本題)

CentralActivity.kt
class CentralActivity : AppCompatActivity(), IActivityLifeCycle, BeaconConsumer {

    private val TAG: String
        get() = CentralActivity::class.java.simpleName

    companion object {
        fun createIntent(context: Context): Intent = Intent(context, CentralActivity::class.java)
    }

    private val mLifeCycle: ActivityLifeCycle = ActivityLifeCycle(this)
    private lateinit var mBinding: ActivityCentralBinding

    private lateinit var mBeaconManager: BeaconManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_central)
        lifecycle.addObserver(mLifeCycle)
    }

    override fun onDestroy() {
        super.onDestroy()
        lifecycle.removeObserver(mLifeCycle)
    }

    override fun onCreated() {
        // BeaconManagerを取得する
        mBeaconManager = BeaconManager.getInstanceForApplication(this)
        // iBeaconの受信設定:iBeaconのフォーマットを登録する
        mBeaconManager.beaconParsers.add(BeaconParser().setBeaconLayout(BeaconUtil.IBEACON_FORMAT))
    }

    /**
     * フォアグラウンド移行時
     */
    override fun onConnected() {
        // Beaconのイベント設定
        mBeaconManager.bind(this@CentralActivity)
    }

    /**
     * バックグラウンド移行時
     */
    override fun onDisconnect() {
        // Beaconのイベント解除
        mBeaconManager.unbind(this@CentralActivity)
    }

    override fun onBeaconServiceConnect() {
        val mRegion = Region(packageName, null, null, null)

        //Beacon領域の入退場を検知するイベント設定
        mBeaconManager.addMonitorNotifier(object : MonitorNotifier {
            override fun didEnterRegion(region: Region) {
                //レンジングの開始
                mBeaconManager.startRangingBeaconsInRegion(mRegion);
            }

            override fun didExitRegion(region: Region) {
                //レンジングの終了
                mBeaconManager.stopRangingBeaconsInRegion(mRegion);
            }

            override fun didDetermineStateForRegion(i: Int, region: Region) {
            }
        })

        // レンジングのイベント設定
        mBeaconManager.addRangeNotifier { beacons, region ->
            beacons
                    .map { "UUID:" + it.id1 + " major:" + it.id2 + " minor:" + it.id3 + " RSSI:" + it.rssi + " Distance:" + it.distance + " txPower" + it.txPower }
                    .forEach { Log.d(TAG, it) }
        }

        try {
            // 入退場検知イベントの登録
            mBeaconManager.startMonitoringBeaconsInRegion(mRegion)
        } catch (e: RemoteException) {
            Log.e(TAG, "Exception", e)
        }
    }
}

※ 一応、Lifecycle-Aware使ってます。(おまけ)

ActivityLifeCycle.kt
class ActivityLifeCycle(lifeCycleCallback: IActivityLifeCycle) : LifecycleObserver {
    private val mCallback = lifeCycleCallback

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun create() {
        mCallback.onCreated()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun resume() {
        mCallback.onConnected()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stop() {
        mCallback.onDisconnect()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun destroy() {
    }
}
IActivityLifeCycle.kt
interface IActivityLifeCycle {

    fun onCreated()

    fun onConnected()

    fun onDisconnect()
}

5. 結果

  • 下記のログを取得できます。(xには、英数字が入ります)
D/CentralActivity: UUID:xxxxxxxx--xxxx-xxxx-xxxx-xxxxxxxxxxxx major:xxx minor:xxx RSSI:-58 Distance:0.018842071701878325 txPower-90

まとめ

運用の煩雑さ等で、普及が滞っているiBeaconですが、
iBeaconの技術を使えば、ユーザーの生活をもっと便利にできそうなアイデアが生まれます。
Bluetooth 5.0の登場で、Bluetoothを使用したIoTのアイデアがさらに生まれることかと思います。

  • 教室にBeaconデバイスを配置して、出欠管理
  • majorとminerに対応した問題を配信する

など、普及の方法も考えたアイデアを、どんどん実現していきたいです。

反省点

  • 公開日までにちゃんと書いておけばよかった
  • NFCのほうが需要あったかも・・・
  • 確認で使った送信の方書けばよかった・・・
  • バックグラウンドの方がネタがあった気がする