문제 상황
현재 블루투스 권한이 없을 때, 스캔을 시도하면 버튼을 눌러도 사용자에게 피드백이 없습니다.
요구 사항
해당 기능을 사용하려고 할 때, 권한 체크를 수행하고 권한이 없을 경우, 다음의 조치를 취해야 합니다.
- 이전에 권한 요청을 했다면
- 권한이 필요한 이유를 팝업으로 표시
- 설정으로 이동할 수 있는 버튼 제공
- 이번이 권한 요청이 처음이면
- 바로 권한을 요청
블루투스 통신을 위해 필요한 절차
- 휴대폰 블루투스가 켜져 있는지 확인하기
- 앱에서 블루투스 관련 권한 받기
- 스캔하기
- 기기 연결하기
블루투스 상태 모델
앱에서 블루투스 상태관리를 위하여 다음의 모델을 사용했습니다.
- DISABLED: 사용자에게 블루투스를 활성화할 것을 요청합니다.
- PERMISSION_DENIED: 사용자가 필요한 권한을 부여하지 않았을 때, 설정으로 유도하여 권한을 요청합니다.
- READY: 블루투스와 모든 권한이 준비되어 있어서 바로 장치 검색이나 연결을 시작할 수 있는 상태입니다.
- SEARCHING: 장치 검색을 진행 중이며, 검색 중에는 이 상태를 유지합니다.
- CONNECTED: 특정 장치에 연결이 완료되어 데이터 교환을 할 수 있는 상태입니다.
- ERROR: 기대하지 않은 문제가 발생했을 때 이 상태로 전환하여 오류 처리 로직을 수행할 수 있습니다.
코드 설명
1. 휴대폰 블루투스 기능 활성화
휴대폰의 블루투스 기능이 켜져 있는지 확인합니다. 블루투스가 꺼져 있다면 사용자가 이를 활성화하도록 요청하는 창을 띄웁니다.
@Composable
fun SetupHomeStateEffects(
bluetoothAdapter: BluetoothAdapter?,
bluetoothEnableLauncher: ActivityResultLauncher<Intent>,
permissionLauncher: ActivityResultLauncher<Array<String>>
) {
val context = LocalContext.current
LaunchedEffect(bluetoothAdapter) {
// Bluetooth 활성화 요청
bluetoothAdapter?.let {
if (!it.isEnabled) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
bluetoothEnableLauncher.launch(enableBtIntent)
}
}
}
LaunchedEffect(Unit) {
// 블루투스 사용 권한
handleBlePermission(permissionLauncher, context)
}
}
2. 블루투스 설정 화면으로 이동
사용자가 블루투스를 활성화하지 않고 원래 화면으로 돌아왔을 경우, 블루투스 설정 화면으로 이동시킵니다.
@Composable
fun RememberBluetoothEnableLauncher(
context: Context,
): ActivityResultLauncher<Intent> =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != Activity.RESULT_OK) {
context.startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))
}
}
3. 블루투스 관련 권한 받기
각 버전에 필요한 권한 리스트를 구성하여 요청합니다.
private fun buildPermissionsList(): MutableList<String> {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
permissions.addAll(
listOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
)
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.addAll(
listOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
)
}
return permissions
}
private fun handleBlePermission(
permissionLauncher: ActivityResultLauncher<Array<String>>,
context: Context
) {
// 버전에 따른 권한 리스트 생성
val permissions = buildPermissionsList()
permissionLauncher.launch(permissions.toTypedArray())
}
4. 설명 다이얼로그
사용자가 이전에 권한 요청을 거절한 적이 있다면, 권한이 필요한 이유를 설명하는 다이얼로그를 띄웁니다. 이를 통해 사용자가 권한의 필요성을 이해하고 설정으로 이동할 수 있도록 합니다.
fun handleBlePermissionDialog(
permissionLauncher: ActivityResultLauncher<Array<String>>,
homeViewModel: HomeViewModel,
context: Context
) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
Manifest.permission.ACCESS_COARSE_LOCATION
)
) {
// 권한을 요청에 대한 추가적인 설명이 필요한 경우
homeViewModel.updateBlePermissionChange(true)
} else {
// 그냥 권한을 요청하는 경우
handleBlePermission(permissionLauncher, context)
}
}
@Composable
fun HandleBlePermissionDialog(homeViewModel: HomeViewModel) {
val context = LocalContext.current
if (homeViewModel.showBlePermissionDialog.value) {
ConfirmationDialog(
title = stringResource(R.string.bluetooth_permission_title),
body = stringResource(R.string.bluetooth_permission_explanation),
acceptBtnTitle = stringResource(R.string.change_now),
declineBtnTitle = stringResource(R.string.change_later),
onDismissRequest = { homeViewModel.updateBlePermissionChange(false) },
onDeclineClick = {
homeViewModel.updateBlePermissionChange(false)
}
) {
homeViewModel.updateBlePermissionChange(false)
// 설명 확인 후 버튼 누르면, 설정 화면으로 이동
navigateToSettings(context)
}
}
}
5. 사용 전 권한 확인하기
BLE 통신을 하는 경우, 권한 상태를 확인하고 없다면 요청합니다.
private fun handleDeviceScanClick(
bluetoothAdapter: BluetoothAdapter?,
homeViewModel: HomeViewModel,
permissionLauncher: ActivityResultLauncher<Array<String>>,
context: Context
) {
if (bluetoothAdapter != null) {
val blePermissionResult = homeViewModel.blePermissionResult.value
if (bluetoothAdapter.isEnabled && blePermissionResult == BLESTATES.READY) {
// 블루투스 스캔
} else if (blePermissionResult == BLESTATES.PERMISSION_DENIED) {
HandleBlePermissionDialog(permissionLauncher, homeViewModel, context)
}
}
}
@Composable
fun RememberPermissionLauncher(
homeViewModel: HomeViewModel
): ActivityResultLauncher<Array<String>> =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val allPermissionsGranted = permissions.entries.all { it.value }
homeViewModel.updateBlePermissionResult(
if (allPermissionsGranted) BLESTATES.PERMISSION_GRANTED else BLESTATES.PERMISSION_DENIED
)
}
6. 홈 화면
위의 코드들을 사용하는 방법입니다.
@Composable
fun HomeScreenContent(
navController: NavController,
homeViewModel: HomeViewModel,
) {
val context = LocalContext.current
val homeState by homeViewModel.homeViewState.collectAsState()
val bluetoothAdapter = RememberBluetoothAdapter(context)
// Initialize and prepare all launchers
// 블루투스 온오프 런처
val bluetoothEnableLauncher = RememberBluetoothEnableLauncher(context)
// 퍼미션 관련 런처
val permissionLauncher = RememberPermissionLauncher(homeViewModel)
// Reactive effects for state updates
SetupHomeStateEffects(
homeViewModel,
bluetoothAdapter,
bluetoothEnableLauncher,
permissionLauncher
)
Column {
HomeHeader(homeState)
HomeContent(navController, homeViewModel, bluetoothAdapter, homeState, permissionLauncher)
}
HandleBlePermissionDialog(homeViewModel)
}
참고링크
Bluetooth permissions | Connectivity | Android Developers
https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview
'질문 정리' 카테고리의 다른 글
안드로이드에서 CRC32로 무결성 체크하기 (0) | 2024.07.02 |
---|---|
안드로이드 PDF 파일 저장하기 구현 (1) | 2024.06.28 |
Compose Snackbar 알림 관리를 위한 Utils (0) | 2024.06.20 |
Intent 정리: 문의하기 클릭 시 이메일 앱으로만 연결 (0) | 2024.06.19 |
Kotlin에서 리스트와 맵의 변환: 순서가 유지될까? (0) | 2024.06.18 |