はじめに
AndroidOS”12″(以降、Android12)からBluetooth周りが大きく変わりました。そこでAndroid12未満もサポートするBluetoothを扱うアプリの作り方ガイドをご紹介します。
大前提:権限周りの話
Android12以上では、”BLUETOOTH_CONNECT, BLUETOOTH_SCAN”の2つの権限を取得する必要があります。取得できていない状態でBluetooth接続依頼ダイアログを表示しようとするとアプリがクラッシュするようになっています。また、BLEでは下記権限が必要になります。
OS | 権限 |
Android12未満 | ACCESS_FINE_LOCATION |
Android12以上 | ACCESS_FINE_LOCATION, BLUETOOTH_CONNECT, BLUETOOTH_SCAN |
アルゴリズム処理
Android12ではアプリがクラッシュするため、アルゴリズムを下記のようにすれば単純化されるでしょう。
権限の取得(BLEで必要なのもこの段階で取ってしまう) -> Bluetooth接続依頼ダイアログの表示 -> BLEスキャン
Manifest設定
//Android12未満を対応させるなら
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
// Android12から求められるpermission
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
// Android12以上, Android12未満両方必要
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION" />
// ここtrueにしたりするとBLEが備わってない端末をそもそも起動させなくさせることもできる
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
権限周りの取得
コールバック用の定数宣言。指定する整数はなんでも構いません。
private val requestBluetoothScanPermissionCode = 600
private val requestAccessFineLocationPermissionCode = 601
権限を取得しているかのチェックコード
// Android12未満
private fun enableAccessFineLocation() =
ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
// Android12以上
@RequiresApi(Build.VERSION_CODES.S)
private fun enableBluetoothScanPermission() =
enableAccessFineLocation() &&
ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_SCAN
) == PackageManager.PERMISSION_GRANTED
権限の取得コード
// Android12未満
private fun checkAccessFineLocation() {
var permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
requestPermissions(
permissions, requestAccessFineLocationPermissionCode
)
}
// Android12以上
// checkAccessFineLocationに"BLUETOOTH_CONNECT, BLUETOOTH_SCAN"
// が増えただけなのでpermissionsをmergeして1つのメソッドで対応するのもOKです
private fun checkBluetoothScanPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
requestPermissions(
permissions, requestBluetoothScanPermissionCode
)
}
}
コールバック処理
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
requestBluetoothScanPermissionCode -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//権限取得時
}
return
}
requestAccessFineLocationPermissionCode -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//権限取得時
}
return
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
上記コードはActivity内ではDeprecatedにはなりませんが、最近の書き方に置き換えると下記コードのようになります。
private fun checkBluetoothScanPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (enableBluetoothScanPermission())
return
val permissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
requestPermissionsResultLauncher.launch(permissions)
}
}
private val requestPermissionsResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { granted ->
if (granted.map { (_, v) -> v }.reduce { res, v -> res && v}) {
// 権限取得時処理
}
}
Bluetooth接続依頼
// adapter周り
private val bluetoothAdapter: BluetoothAdapter? by lazy { // API level18から使える
(getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
}
private val bleScanner: BluetoothLeScanner? by lazy {
bluetoothAdapter?.bluetoothLeScanner
}
// Bluetooth接続要求
private fun turnOnBluetooth() {
bluetoothAdapter?.takeIf { !it.isEnabled }?.apply {
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activityResultLauncher.launch(intent)
}
}
// 後処理
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
if (result.resultCode == RESULT_OK) {
// Bluetooth ON時の後処理
}
}
BLE
BLEスキャンのコード
// フラグで状態を管理した方が良いです(理由は後述)
private var runScaning: Boolean = false
private fun scanLeDevice(enable: Boolean) {
// bleScanner?.startScanで求められるため必要
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED
) {
return
}
}
// すでに走っている時は一度stopしてから再度走らせる
if (enable && runScaning) {
runScaning = false
scanLeDevice(false)
}
when (enable) {
true -> {
runScaning = true
bleScanner?.startScan(leScanCallback)
}
else -> {
runScaning = false
bleScanner?.stopScan(leScanCallback)
}
}
}
状態を管理すべき理由は長期に渡りBLEスキャンを走らせているとコールバックが届かなくなる問題があり、対策としてhandlerなどでスキャン処理のループ状態を作り、10分置きなどにBLEスキャンをやり直すなどがあります。従って、状態を管理しておけば、2重で走らせてしまう問題などがケアできます。
コールバックは下記のようになります。
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
if (result == null)
return
val rssi = result.rssi
result.scanRecord?.bytes?.let {
// 9~24byteがuuids
// 25,26byteがmajor
// 27,28byteがminor
// ※major, minorは16進数なので注意
val tmp = it.map { String.format("%02X", it) }
val uuid = tmp.slice(9..24).joinToString("")
val major = tmp.slice(25..26).joinToString("")
val minor = tmp.slice(27..28).joinToString("")
}
super.onScanResult(callbackType, result)
}
override fun onBatchScanResults(results: List<ScanResult?>?) {
super.onBatchScanResults(results)
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
}
さいごに
Bluetooth周りはAndroid12で大きく変更されたので今後も大きく変更される可能性があります。一度公式の情報に目を通してからの実装をオススメします。