Android App을 개발하다보면 특정 기능 사용을 위해 권한 요청이 필요한 경우가 있다.
예전(마쉬멜로우 이전)에는 코드상에 권한 허가만 넣어주면 쉽게 기능을 이용할 수 있었지만, 이젠 사용자가 직접 권한을 허락해야만 특정 권한 기능을 사용할 수 있도록 바뀌었다.
Android Permission의 종류는 Component를 보호하고 수준인 protectionLevel에 따라 normal, signature, dangerous 세 가지로 나뉘며 각각의 특성은 다음과 같다.
1. 일반 권한 (protectionlevel = "normal")
낮은 수준의 보호 권한으로써 App 사용자(User)에게 권한 부여 요청을 필요로 하지 않고 App 설치 시 자동으로 권한을 부여 받는다.
ex) android.permission.INTERNET
2. 서명 권한 (protectionlevel = "signature", "signatureOrSystem")
ex) 특정 App의 Provider 이용을 위한 권한 설정
3. 런타임 권한 (protectionlevel = "dangerous")
높은 수준의 보호 권한으로써 해당 권한을 통해 부여받는 기능은 시스템이나 다른 App에 미치는 영향이 크고 사용자 개인 정보에 접근할 수 있기 때문에 사용자에게 권한 부여 요청을 필요로 한다.
이와 같이, 3. 런타임 권한의 경우 특정 기능(Component)을 Android System에서 보호하고 있기 때문에 해당 기능을 사용하는 App은 AndroidManifest.xml에 해당 기능에 대한 권한을 <uses-permission>
으로 선언한 뒤 User가 직접 해당 권한을 허용하도록 요청해야 한다.
Runtime 권한을 요구하는 주요 Permissions
Permission | 내용 |
---|---|
카메라 | <uses-permission android:name="android.permission.CAMERA"/> |
위치 정보 | <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> |
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION_LOCATION"/> | |
녹음 | <uses-permission android:name="android.permission.RECORD_AUDIO"/> |
전화 | <uses-permission android:name="android.permission.READ_PHONE_STATE"/> |
<uses-permission android:name="android.permission.MODIFY_PHONE_NUMBER"/> | |
<uses-permission android:name="android.permission.CALL_PHONE"/> | |
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/> | |
<uses-permission android:name="android.permission.READ_CALL_LOG"/> | |
문자 | <uses-permission android:name="android.permission.SEND_SMS"/> |
<uses-permission android:name="android.permission.RECEIVE_SMS"/> | |
<uses-permission android:name="android.permission.CALL_PHONE"/> | |
<uses-permission android:name="android.permission.READ_SMS"/> | |
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH"/> | |
<uses-permission android:name="android.permission.RECEIVE_MMS"/> | |
캘린더 | <uses-permission android:name="android.permission.READ_CALENDAR"/> |
<uses-permission android:name="android.permission.WRITE_CALENDAR"/> |
그리고, 만약 User가 해당 권한을 허용하지 않은 채 보호되고 있는 Component 기능을 이용한다면, 다음과 같은 Error를 throw하게 되고, 개발자는 해당 상황을 고려한 App Logic을 작성해야한다.
CameraService: Permission Denial: can't use the camera pid=11026, uid=10355
AndroidRuntime: Process: com.android.sample.camera, PID: 11026
AndroidRuntime: java.lang.SecurityException: validateClientPermissionsLocked:1547: Caller "com.android.sample.camera" (PID 10355, UID 11026) cannot open camera "0" without camera permission
AndroidRuntime: at android.hardware.camera2.CameraManager.throwAsPublicException(CameraManager.java:1740)
AndroidRuntime: at android.hardware.camera2.CameraManager.openCameraDeviceUserAsync(CameraManager.java:902)
AndroidRuntime: at android.hardware.camera2.CameraManager.openCameraForUid(CameraManager.java:1148)
AndroidRuntime: at android.hardware.camera2.CameraManager.openCameraForUid(CameraManager.java:1169)
AndroidRuntime: at android.hardware.camera2.CameraManager.openCamera(CameraManager.java:1007)
AndroidRuntime: at com.android.sample.camera.cameraDevice.openCamera(CameraUnit.kt:181)
AndroidRuntime: Caused by: android.os.ServiceSpecificException: validateClientPermissionsLocked:1547: Caller "com.android.sample.camera" (PID 10355, UID 11026) cannot open camera "0" without camera permission (code 1)
AndroidRuntime: at android.hardware.ICameraService$Stub$Proxy.connectDevice(ICameraService.java:808)
AndroidRuntime: at android.hardware.camera2.CameraManager.openCameraDeviceUserAsync(CameraManager.java:878)
Android API 30 이전까진 ActivityCompat에서 제공하는 requestPermissions
과 Activity 및 Fragment에서 지원하는 onRequestPermissionsResult
를 통해 권한을 아래와 같이 획득할 수 있다.
const val CAMERA_PERMISSION_CODE = 999
class CameraActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
checkCameraPermission()
}
// 1. Camera 권한 확인
private fun checkCameraPermission() {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
requestCameraPermission()
}
}
// 2. Camera 권한 요청
private fun requestCameraPermission() {
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA), CAMERA_PERMISSION_CODE)
}
// 3. Camera 권한 처리
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) = when (requestCode) {
CAMERA_PERMISSION_CODE -> {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera()
} else {
// 권한 요청 거부했을 경우의 logic 처리
// ex) permission 재요청 or 무시 등등
}
}
}
// 4. Camera 권한 획득 후 처리되는 logic
private fun startCamera() {
// ...
}
}
앞선 sample code를 보면, 요청했던 모든 권한은 onRequestPermissionsResult
한 곳에서 처리되는 모습을 볼 수 있다.
이는, 권한 요청 flow가 Activity 및 Fragment Lifecycle에 밀접하게 연관되어있음에도 불구하고 App에서 권한 요청을 처리하는 방법이 명확하지 않아 구현/관리가 복잡해질 수 있으며 여러 권한을 함께 처리하는 데 제한적이게 된다.
ex) Camera 권한의 경우 보통 Storage 권한과 함께 처리되야 한다.
이러한 연유로, Android에선 명확하고 독립적인 권한 요청 Flow를 지원하기 위해 API 30 이상부터 해당 방식을 통한 권한 요청은 deprecated 되었다.
가장 먼저, Android에선 개발자가 보다 나은 권한 요청 logic을 작성하고 제공할 수 있도록 다음과 같은 Permission workflow를 제공하고 있다.
참고로 Android에선 Permission에 대한 권한 요청은 사용자 사용성을 고려하여 두 번만 실행할 수 있다.
따라서, 한번 거절된 Permission을 재요청할 때 rationale을 통해 해당 Permission이 필요한 이유를 사용자에게 노출시키고 또 거절된 경우엔 App을 종료시키지 않고 해당 feature에 대해 degrade된 사용성을 가지도록 권장하고 있다.
즉, 앞선 Permission workflow는 다음과 같이 요약할 수 있으며
(1) 최초로 권한을 요청하는 경우
(2) 거절당한 권한을 다시 요청하는 경우
(3) 거절과 동시에 해당 권한요청을 다시 표시하지 않음 옵션을 선택한 경우
해당 flow에 대한 API 30 이후 기반 sample code는 다음과 같다.
AndroidManifest.xml에 요청 될 permission은 이미 선언 되었음을 가정한다.
class MyActivity : AppCompatActivity() {
private val cameraPermissionLauncher : ActivityResultLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// 권한이 허용된 경우에 실행할 코드
} else {
// 권한이 거부된 경우에 실행할 코드
// ActivityCompat.shouldShowRequestPermissionRationale
// → 사용자가 권한 요청을 명시적으로 거부한 경우 true를 반환한다.
// → 사용자가 다시 묻지 않음 선택한 경우 false를 반환한다.
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
// 권한 요청에 대한 이유를 사용자에게 설명하는 Dialog를 표시
AlertDialog.Builder(this)
.setTitle("권한 요청")
.setMessage("카메라 권한이 필요합니다.")
.setPositiveButton("확인") { _, _ ->
requestCameraPermission.launch(Manifest.permission.CAMERA)
}
.setNegativeButton("취소") { _, _ ->
// Dialog에서 취소 버튼을 누른 경우에 실행할 코드
}
.show()
} else {
// 사용자가 권한 요청 다이얼로그에서 "다시 묻지 않음" 옵션을 선택한 경우에 실행할 코드
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 카메라 권한이 허용되어 있는지 확인
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
// 권한이 이미 허용된 경우에 실행할 코드
} else {
// 권한이 허용되어 있지 않은 경우 권한 요청 다이얼로그를 표시
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
이렇게 Android API 30 이후부터는 ActivityResultLauncher
를 통해서 App의 Permission을 제어하고 있는 것을 알 수 있다.
Permission launcher를 개발자가 직접 구현함으로써 각각의 Permission 성격에 따라 Runtime에 유연하고 독립적으로 처리할 수 있는 환경이 마련된 것이다.
그리고, 복수개의 Permission을 한번에 요청하는 sample code는 다음과 같이 ActivityResultContracts.RequestMultiplePermissions()
를 사용하여 처리할 수 있으니 참고하도록 하자.
val multiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach { (permission, isGranted) ->
when {
isGranted -> {
// 권한이 승인된 경우 처리할 작업
}
!isGranted -> {
// 권한이 거부된 경우 처리할 작업
}
else -> {
// 사용자가 "다시 묻지 않음"을 선택한 경우 처리할 작업
}
}
}
// multiple permission 처리에 대한 선택적 작업
// - 모두 허용되었을 경우에 대한 code
// - 허용되지 않은 Permission에 대한 재요청 code
}
val permissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_CONTACTS,
...
)
multiplePermissionsLauncher.launch(permissions)
- android-developer : Runtime 권한 요청
- https://copycoding.tistory.com/357
- https://full-stack.tistory.com/entry/Android-Permission-%EA%B6%8C%ED%95%9C-1-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B6%8C%ED%95%9C-feat-Bluetooth
- https://juahnpop.tistory.com/217
- https://velog.io/@changhee09/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%8D%BC%EB%AF%B8%EC%85%98Permission