vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Android】Kotlin + Bluetooth Low Energy 1

はじめに

これまでも何度かAndroidでのBluetoothLowEnergy(BLE)を試してきましたが、ふともう一度細かい部分にも気をつけながら再挑戦してみることにしました。
ただ、せっかくなので興味があったKotlinを使ってみることにしました。

Activityを開く

変数の宣言などは異なっていても、基本的にはJAVAと同じように書けるのかな?と思っていたら盛大につまづきました。

画面遷移にはIntentを使用するのですが、(おそらく)1.0からは以下のように書く必要があります。

var intentCentral = Intent(this, CentralActivity::class.java)
startActivity(intentCentral)

これを「javaClass」と書こうとしてエラーに阻まれていたのですが、JAVAで一旦コードを書いて自動変換する、という方法で見つけることができました。

自動変換、便利ですねぇ。

位置情報

2つ目に引っかかったのがこの位置情報(GPS)です。

以前JAVAで作成したサンプルを元にCentral側のコードを書き、 Nexus5Xで動作させるとエラーはでないのにスキャンがされない(ScanCallbackのonScanResultが呼ばれない)、という問題が発生しました。

もしやAndroidWearにつないでいるのが原因か?など色々調べてみたところ、以下に行き着きました。

どうやらtargetSdkVersion 23からは、デバイスのスキャンをするにはGPSをオンにする必要があるようです。
22までのバージョンでも、位置情報のパーミッション自体は必要になっていましたが、更に変更があったと。

関連するクラスにて使用する、ということのようですが、実際何に使っているのでしょうね。
iOSでiBeaconを利用するときにも位置情報を使用していた気がしますが、この辺りと関連しているのでしょうか。

とにかく以前と同じようにGPSをオンにできるようにします。

LocationAccesser.kt

import android.content.IntentSender
import android.os.Bundle
import android.support.v7.app.AlertDialog

import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.gms.location.LocationSettingsStatusCodes

class LocationAccesser : GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
    private var apiClient: GoogleApiClient? = null
    final var REQUEST_NUM_LOCATION = 2
    override fun onConnectionFailed(result: ConnectionResult) {
    }
    override fun onConnectionSuspended(cause: Int) {
    }
    override fun onConnected(bundle: Bundle?) {
    }
    fun checkIsGpsOn(activity: CentralActivity) {
        // OS Version 6.0以降はGPSがOffだとScanできないのでチェック.
        val locationRequest = LocationRequest.create()
        locationRequest.priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
        locationRequest.interval = 3000L
        locationRequest.fastestInterval = 500L
        val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
        builder.setAlwaysShow(true)

        if (apiClient == null) {
            apiClient = GoogleApiClient
                    .Builder(activity.applicationContext)
                    .enableAutoManage(activity, this)
                    .addApi(LocationServices.API)
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .build()
        }
        val result = LocationServices.SettingsApi.checkLocationSettings(apiClient, builder.build())
        result.setResultCallback {
            settingsResult ->
                val status = settingsResult.status
                when (status.statusCode) {
                    LocationSettingsStatusCodes.SUCCESS ->{
                        // GPSがOnならScan開始.
                        activity.onGpsEnabled()
                    }
                    LocationSettingsStatusCodes.RESOLUTION_REQUIRED ->{
                        try {
                            // GPSがOffならIntent表示. onActivityResultで結果取得.
                            status.startResolutionForResult(
                                    activity, REQUEST_NUM_LOCATION)
                        } catch (ex: IntentSender.SendIntentException) {
                            activity.runOnUiThread {
                                val alert = AlertDialog.Builder(activity)
                                alert.setTitle(activity.getString(R.string.error_title))
                                alert.setMessage(ex.message)
                                alert.setPositiveButton(activity.getString(android.R.string.ok), null)
                                alert.show()
                            }
                        }
                    }
                    LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> {
                        // Locationが無効なら無視.
                    }
            }
        }
    }
}

接続を切る

以前作成したサンプルでは、一度Peripheralデバイスに接続したあとActivityを閉じると、次にCentralのActivityを開いても再接続ができませんでした。
これは最初に接続した状態が保持されるためで、再接続するためには一旦接続を切る必要があります。

CentralActivity.kt

~省略~
    override fun onDestroy(){
        super.onDestroy()

        // reset.
        if(bleAdapter!!.isEnabled) {
            bleScanner!!.stopScan(bleScanCallback)
            // 接続中のデバイスがあれば切断して閉じる.
            if (!bleManager!!.getConnectedDevices(BluetoothProfile.GATT).isEmpty()) {
                bleGatt!!.disconnect()
                bleGatt!!.close()
            }
            bleGatt = null
        }
    }
~省略~

なお、Bluetoothがオフの状態でstopScanなどを実行すると、NullPointerExceptionが出たりします。

終わりに

2回めのKotlinへの挑戦となりましたが、すこしずつ慣れてくるとコード量が減って良いのかも、という気になってきました。
今回はCentral側のプログラムでしたが、次回はPeripheral側を作成する予定です。

参考