[Android] 권한(Permission) 처리하기
Android 5.1 버전 이하에서는 앱의 설치 단계에서만 사용자에게 권한을 요청한다. 개별적으로 권한을 선택할 수 없어서 앱을 사용하기 위해서 요청된 묶음 권한에 대해 감수할 수밖에 없었다. 또한 사용자가 앱 사용 중 어느 타이밍에 해당 권한을 사용하는지 확실히 알 수 없어서 약간의 찝찝한(불안한)을 가질 수밖에 없었다.
하지만 Android 6.0 버전부터 앱 실행 중 사용자에게 권한을(런타임 권한) 요청할 수 있는 기능이 추가됐다. 사용자의 입장에서 어떤 동작을 할 때 권한 요청을 받기 때문에 이 권한이 왜 필요한지 이유를 알 수 있다. (사용자가 편안함을 느낀다고 한다.)
안드로이드 버전이 업데이트될 때 어떤 기능에 대해서 권한을 체크하도록 추가되는 경우가 있다. 예를 들어 Android 12 버전에는 블루투스가 추가되었고, 이번에 Android 13 버전(베타)에는 알람 권한이 추가된다고 한다.
그리고 시스템 권한 대화상자도 바뀌고 있다. Android 11 버전부터 특정 권한에 대해 거부를 두 번 이상하게 될 경우 '거부 및 다시 묻지 않음' 처리를 하여 시스템 권한 대화상자가 더 이상 나오지 않는다.
권한 요청은 사용자에게 있어 민감한 정보를 요구하는 행위이기 때문에 앱에서 권한을 요청하는 이유를 사용자가 알 수 있도록 표현해야 한다.
권한 처리 예제
예제로 카메라와 블루트스의 권한 요청에서 허용, 허용안함, 거부 및 다시 묻지 않음을 처리해보려고 한다.
1. API 설치 및 SDK sync
2. Manifest에 사용할 권한 등록
3. ContextCompat.checkSelfPermission로 권한이 허용되었는지 체크
4. 권한 허용이 안됐을 경우 requestPermissions를 통해 런타임 권한 요청
5. onRequestPermissionsResult를 통해서 처리 결과를 핸들링
API 설치 및 SDK sync
카메라는 Anroid 6, 블루투스는 Android 12 버전부터 적용되었다. 안드로이드에서 권한을 처리해주려면 안드로이드 API 레벨을 확인해야 한다. 블루투스의 경우 API 레벨은 31이기 때문에 프로젝트의 SDK 버전을 맞춰야 한다.
안드로이드 스튜디오 상단 탭의 Tools → SDK Manager → SDK Platforms 를 거쳐 권한에 맞는 API Level를 설치하면 된다. 이후에 gradle에서 compileSdk와 targetSdk를 설치한 API Level로 올려주고 sync를 해주면 작업을 시작할 수 있다.
Manifest에 권한 등록
<!-- 블루투스 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 카메라 -->
<uses-permission android:name="android.permission.CAMERA" />
요청할 권한 array에 담기
// 버전에 따라 체크할 권한 array 생성
val permissionArray =
if (Build.VERSION.SDK_INT >= 31) {
arrayOf(
Manifest.permission.BLUETOOTH_ADVERTISE, // 블루투스
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.CAMERA // 카메라
)
}else {
arrayOf(
Manifest.permission.CAMERA // 카메라
)
}
권한이 허용됐는지 한 번에 확인하기 위해서 array에 담았는데, 하나씩 처리하고 싶다면 담을 필요는 없다.
권한 허용됐는지 체크
val btnCheckPermission = findViewById<Button>(R.id.btn_permission_request)
btnCheckPermission.setOnClickListener {
if(Build.VERSION.SDK_INT >= 31){
// 블루투스와 카메라 권한이 허용되었는지 체크
if(permissionArray.all{ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED}){
Toast.makeText(this, "블루투스와 카메라 권한이 모두 허용되어 있습니다.", Toast.LENGTH_SHORT).show()
}
// 권한 요청
else{
requestPermissions(permissionArray, REQUEST_PERMISSION_CODE_1)
}
}
else if(Build.VERSION.SDK_INT < 31 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
// 카메라 권한이 허용되었는지 체크
if(permissionArray.all{ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED}){
Toast.makeText(this, "카메라 권한이 허용되어 있습니다.", Toast.LENGTH_SHORT).show()
}
// 권한 요청
else{
requestPermissions(permissionArray, REQUEST_PERMISSION_CODE_2)
}
}
}
버튼을 눌렀을 때 checkSelfPermission으로 권한이 허용됐는지 체크하고 허용이 안됐으면 reqeustPermissions를 호출하여 권한을 요청하도록 했다.
참고로 PackageMangaer.PERMISSION_GRANTED는 권한 허용, PackageMangaer.PERMISSION_DENIED는 권한 거절 상태를 나타낸다.
런타임 권한 처리 결과 핸들링
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode) {
// 다시 묻지 않음을 생각하고 처리하는 법.
REQUEST_PERMISSION_CODE_1 -> {
if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[3] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "모든 권한을 허용하였습니다.", Toast.LENGTH_SHORT).show()
}else{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "블루투스 권한을 허용하였습니다.", Toast.LENGTH_SHORT).show()
}else if(shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_ADVERTISE)){
Toast.makeText(this, "블루투스 권한을 거절하였습니다.", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "블루투스 권한을 다시 묻지 않음을 하였습니다.", Toast.LENGTH_SHORT).show()
}
if(grantResults[3] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "카메라 권한을 허용하였습니다.", Toast.LENGTH_SHORT).show()
}else if(shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)){
Toast.makeText(this, "카메라 권한을 거절하였습니다.", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "카메라 권한을 다시 묻지 않음을 하였습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
// 다시 묻지 않음을 생각하지 않고 처리.
REQUEST_PERMISSION_CODE_2 -> {
// 권한 허용일 때
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "카메라 권한을 허용하였습니다.", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "카메라 권한을 거절하였습니다.", Toast.LENGTH_SHORT).show()
}
}
}
}
런타임 권한 결과를 onRequesetPermissionsResult로 받게 되는데 array에 담았던 순서대로 grantResults에 담긴다.
그래서 카메라와 블루투스 권한이 허용됐는지 확인할 때 grantResults[0], grantResults[3]을 썼다.
사용자가 권한을 허용했는지 거절했는지는 GRANTED와 DENIED로 확인할 수 있지만 '거부 및 다시 묻지 않음'은 상태값을 따로 주어지지 않는다. 이때 필요한 것이 shouldShowRequestPermissionRationale이다.
- shouldShowRequestPermissionRationnale 반환 값
- false → 권한 요청 최초 시도 또는 '거부 및 다시 묻지 않음' 인 경우
- true → 권한 요청 최초 시도 이후에 명시적으로 거부한 경우
조건 분기를 권한을 허용했는지 확인 → 권한을 거절했는지 확인 → 권한을 다시 보지 않음 처리했는지 확인 순으로 탔다.