Android 12 の新しい Bluetooth 権限の実装
問題点
Android 12 で Bluetooth 権限システムが大幅に変更され、多くの開発者が BLE デバイスの検出や接続に問題を経験しています。単に新しい権限をマニフェストに追加するだけでは不十分で、適切なランタイム権限リクエストと設定が必要です。
解決策
マニフェスト設定
まず、AndroidManifest.xml に以下の権限を追加します:
<!-- 古いデバイス向けの従来の Bluetooth 権限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 新しい Android 12 権限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 位置情報権限(Android 11以前) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30" />
<!-- オプションのハードウェア機能 -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
重要な注意点
BLUETOOTH_SCAN
権限に neverForLocation
フラグを追加することで、位置情報を使用しないことを明示できます:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
ランタイム権限のリクエスト
Kotlin での実装例
// 権限リクエストのためのランチャー
private val requestMultiplePermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach { permission ->
if (permission.value) {
// 権限が許可された
when (permission.key) {
Manifest.permission.BLUETOOTH_SCAN -> startBluetoothScan()
Manifest.permission.BLUETOOTH_CONNECT -> connectToDevice()
}
} else {
// 権限が拒否された
showPermissionDeniedMessage()
}
}
}
// Bluetooth 権限をリクエストする関数
fun requestBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12以上の場合
val permissionsToRequest = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
requestMultiplePermissions.launch(permissionsToRequest)
} else {
// Android 11以前の場合
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
}
// 権限チェック関数
private fun checkBluetoothPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED
} else {
// 古いバージョンでは位置情報権限を確認
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
Java での実装例
// 権限リクエスト定数
private static final int REQUEST_BLUETOOTH_PERMISSIONS = 100;
// 権限チェックとリクエスト
private void checkAndRequestBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this,
Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
},
REQUEST_BLUETOOTH_PERMISSIONS);
} else {
// 権限が既に許可されている場合
proceedWithBluetoothOperations();
}
} else {
// Android 11以前の処理
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_BLUETOOTH_PERMISSIONS);
} else {
proceedWithBluetoothOperations();
}
}
}
// 権限リクエスト結果の処理
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_BLUETOOTH_PERMISSIONS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 権限が許可された
proceedWithBluetoothOperations();
} else {
// 権限が拒否された
Toast.makeText(this, "Bluetooth権限が必要です", Toast.LENGTH_SHORT).show();
}
}
}
Jetpack Compose での実装
@Composable
fun BluetoothPermissionHandler() {
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
)
// ライフサイクルイベントの観察
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START &&
!permissionsState.allPermissionsGranted) {
permissionsState.launchMultiplePermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// 権限状態の確認
LaunchedEffect(permissionsState.allPermissionsGranted) {
if (permissionsState.allPermissionsGranted) {
// 権限が許可された場合の処理
startBluetoothOperations()
}
}
}
重要な考慮事項
注意点
近接デバイスの権限: Android 13では「近接デバイス」権限も必要になる場合があります。ユーザーが手動でアプリ設定からこの権限を有効にする必要があるかもしれません。
バックグラウンドでの位置情報: バックグラウンドでBLEスキャンを行う場合、追加の位置情報権限が必要になる場合があります。
ターゲットAPIレベル:
targetSdkVersion
を 31以上に設定していることを確認してください。
トラブルシューティング
アプリがBLEデバイスを検出できない場合:
- すべての必要な権限がマニフェストで宣言されているか確認
- ランタイム権限が正しくリクエストされ、許可されているか確認
- ユーザーが手動で「近接デバイス」権限を有効にする必要がないか確認
- Android 12以上のデバイスで実際にテストする
まとめ
Android 12の新しいBluetooth権限システムでは、マニフェストでの宣言に加えて、適切なランタイム権限リクエストの実装が不可欠です。バージョンに応じた条件分岐と、ユーザーへの明確な説明を提供することで、シームレスなBluetooth体験を提供できます。
詳細はAndroid Developer公式ドキュメントを参照してください。