안드로이드의 permission은 초기에는 Manifest에 추가만 해주면 해결 됐지만, 보안이 강화되면서 API 23부터 사진을 찍거나 위치정보를 이용하는등의 경우에 런타임으로 요청을 해서 사용자 확인을 받아야 한다. Runtime Permissions는 dangerous permissions라고도 하며, 시스템이나 다른앱에게 영향을 줄 수 있는 영역에 접근할 때 필요하다. runtime 으로 permission을 요청하면 사용자에게 다음과 같은 대화상자가 표시된다.

사용자는 허용/거부를 선택할 수 있고 그에 따라 permission이 주어진다.
이러한 이유로, permission을 사용할 때 protection level을 알아야 한다. 만약, dangerous라면 런타임에 request permission이 필요하고, 이를 구현해야 한다. protection level은 레퍼런스 문서에 permission 리스트를 보면 다음과 같이 확인 할 수 있다.

사용자가 허용한 경우에도 오랜 시간이 지나면, permission이 사라질 수 있다. 여기서는 runtime permission 요청하는 구현방법을 살펴볼 것이다. 전반적인 내용은 공식 문서( https://developer.android.com/training/permissions/requesting#manage-request-code-yourself ) 에서 확인할 수 있다. 자세한 설명은 없지만, codelab에 따라할 수 있는 request permisssion구현 방법 을 참고하면 도움이 될 것이다.
Permission 요청은 다음과 같이 진행된다.

흐름을 따라가보면, 최초에 사용자에게 요청한적이 없으므로 permission이 없을것이다. shouldShowRequestPermissionRationale()는 사용자에게 권한의 필요성을 설명하는 UI를 띄워야 하는지 알려주는데, 거부된적이 있을 때 true가 되지만, 처음이므로 false를 돌려줄 것이다. Permission 요청은 RequestPermission()에 의해 진행된다. 요청시, 앞서 살펴본 “Allow/deny”를 선택하는 다이얼로그를 띄운다. 요청 과정은 시스템에서 제공하는 외부 Activity를 띄우는 과정으로 registerForActivityResult()를 이용해 이루어진다. permission요청에 대한 default contract가 제공되기 때문에 이를 이용하면 간단하게 사용가능하다.
Permission 요청을 하면, 사용자에게 ‘허용’, ‘거부’가 뜬다. 여기서 사용자가 ‘허용’을 선택하면 문제없이 다음단계로 진행되어 실행하면 된다. 사용자가 ‘거부’를 하는 경우에는 permission이 필요한 기능만 제외하고 앱의 정상 사용이 가능해야 한다. ‘거부’ 이후에 다시 진입시, shouldShowRequestPermissionRationale()이 true를 리턴하며, 사용자에게 permission이 필요한 이유를 설명하는 다이얼로그를 띄워야 한다. 여기서 확인을 누를시, request permission으로 넘어간다.
Permission 요청시 이전에 거부한적이 있다면, ‘거부, 다시 보이지 않음’ 선택지가 추가로 표시되고, 이걸 사용자가 선택하면 requestPermission()을 호출해도 사용자에게 아무것도 보여주지 않는다. 앱에서는 permission을 획득할 방법이 전무해지게 되는데, 이걸 되돌릴려면 사용자가 앱 설정에 들어가 직접 권한을 수정해야 한다.
하나의 permission을 요청하는 방법
이제 구현방법을 살펴보자. 예제는 코드랩에 올라와 있는 예제를 사용하겠다.
1. 요청할 permission을 manifest 파일에 추가
런타임으로 요청할 permission이라도 일단, manifest파일에 추가 되어야 한다. 예제에 있듯 카메라기능을 추가해보자.
<uses-permission android:name="android.permission.CAMERA" />
2. request permission의 흐름 구현
위에서 살펴본 플로우차트가 좀 복잡하게 보일지도 모르겠다. 공식문서의 플로우차트나 같은내용인데, 좀 더 실제 구현에 가깝게 그려놓은 것이다. 실제 구현은 when문을 이용하면 좀 더 간단하게 정리된다. 예제를 보자.
fun onClickRequestPermission(view: View) {
when {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
layout.showSnackbar(
view,
getString(R.string.permission_granted),
Snackbar.LENGTH_INDEFINITE,
null
) {}
}
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.CAMERA
) -> {
layout.showSnackbar(
view,
getString(R.string.permission_required),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.ok)
) {
requestPermissionLauncher.launch(
Manifest.permission.CAMERA
)
}
}
else -> {
requestPermissionLauncher.launch(
Manifest.permission.CAMERA
)
}
}
}
버튼 클릭시, permission을 요청하도록 구현된 내용이다. when문의 흐름은 위에서 아래로 순서대로 진행되며, 조건에 부합되면 리턴된다.
플로우 차트와 동일하게, 시작은 권한의 유무를 체크하는 것이다. 코드를 보면, ContextCompat.checkPermission()을 이용하여 Camera permission을 확인하고 있으며, PERMISSION_GRANTED가 리턴되면 권한이 있는 것으로 정상 실행된다.
권한 체크를 했는데, 권한이 없는 경우, shouldShowRequestPermissionRationale()을 해당 permission에 대해 확인한다. 여기서는 CAMERA permission을 확인하고 있다. true가 리턴되면, 여기서는 간단하게 snackbar를 이용하여 권한의 필요성을 표시하고, OK를 하는 경우 request permission을 수행한다. snackbar나 request permission 부분의 구현은 뒤에 설명하겠다.
앞서 체크한 조건들을 만족하지 않는다면, 권한이 없고, Rationale표시가 필요없다는 얘기이므로, 바로 request permission을 수행한다.
생각보다 흐름이 단순하게 구현되는걸 확인할 수 있다. 그러면, permission요청시 사용되는 requestPermissionLauncher를 살펴보자.
3. requestPermissionLauncher 의 구현
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
Log.i("Permission: ", "Granted")
} else {
Log.i("Permission: ", "Denied")
}
}
registerForActivityResult()는 바로 이전 포스트에서 설명한대로 startActivityForResult()와 onActivityResult()가 deprecated되고 사용되는 방법이다. 이게 뭔지 모르겠다면 해당 포스트를 참조하기 바란다.
registerForActivityResult()에 RequestPermssion() 이라는 contract를 넘겨주고 있다. 이는 미리 정의된 default contract로 permission요청시 사용된다. 리턴값이 Boolean으로 사용자가 ‘허용’했는지, ‘거부’했는지를 돌려준다. callback함수로는 lambda function을 써서 괄호 밖에 기술하고 있다. 샘플코드로 로그를 찍는 것 외에 하는일이 없지만, 리턴값으로 받는 isGranted가 true이면 허용된 것으로 정상적인 실행을 진행하면 되고, false이면 거부한 것이므로 해당기능을 사용하지 못하는 상태로 진행하면 된다.
이렇게 registerForActivityResult()로 리턴된 launcher를 가지고 있다가 저 위의 구현에서 보듯, 필요시에 launcher.launch( Manifest.permission.CAMERA )를 이용하여 permission을 요청하게 된다.
4. 예제에서 사용된 snackbar의 구현
마지막으로, 예제에서 사용하는 Snackbar의 구현은 extension function으로 구현되어 있으며 다음과 같다.
fun View.showSnackbar(
view: View,
msg: String,
length: Int,
actionMessage: CharSequence?,
action: (View) -> Unit
) {
val snackbar = Snackbar.make(view, msg, length)
if (actionMessage != null) {
snackbar.setAction(actionMessage) {
action(this)
}.show()
} else {
snackbar.show()
}
}
여러개의 permission을 요청하는 경우.
끝내기전에 이런 의문이 들지도 모르겠다. 여러개의 permission을 요청하는 경우라면? registerForActivityResult()에서 다음과 같이 RequestMultiplePermissions contract를 이용하면 된다.
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantedMap ->
for(item in grantedMap) {
when(item.key) {
android.Manifest.permission.CAMERA -> {
Log.i("Permission: ", "CAMERA is granted : ${item.value}")
}
android.Manifest.permission.ACCESS_FINE_LOCATION -> {
Log.i("Permission: ", "FINE_LOCATION is granted : ${item.value}")
}
android.Manifest.permission.ACCESS_COARSE_LOCATION -> {
Log.i("Permission: ", "COARSE_LOCATION is granted : ${item.value}")
}
}
}
}
차이점이라면, 여러개의 permission을 처리해야 하므로 반환되는 값이 map<String, Boolean>의 형태로 되어있다. permission 이름 String과 granted 여부가 Boolean으로 표시된다. 위 예제에 보듯, 각 permission에 대해 허용 여부를 확인 할 수 있다.
아, 한가지 더, launcher를 실행 할 때, 당연하게도 여러개의 permission을 넣어줘야 한다.
requestPermissionLauncher.launch(
arrayOf(android.Manifest.permission.CAMERA,
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION)
)
앞에서 CAMERA하나만 넘겨주던 부분에 permission string들에 대한 array로 넘겨주고 있다.
생각해볼 문제?
이것으로 정리는 했는데, 끝내기 전에 생각해봐야할 문제가 하나 있긴 하다. 사용자가 permission을 거부하고, 또 거부하면서 다시표시 안함을 선택했다면, 앱에서는 permission을 다시 요청할 방도가 없다. 필요하다면, 사용자가 직접 권한을 변경하도록 앱 설정으로 안내를 하는 방법이 있을 수 있긴한데, 이 상태를 체크할 수 있는 방법이 있는지도 의문이다. 근본적으로 이걸 고민해야 하는지 자체가 고민이다. 선택은 당신의 몫 🙂
Update 1 (2021.10.26)
사용자가 permission을 거부하고, 다시표시 안함을 선택한 경우에 shouldShowRequestPermissionRationale()이 false를 리턴한다. 그러니까, 다시표시 안함 상태에서 request permission을 하면 사용자에게 아무것도 보여주지 않고 응답이 deny가 될 것이다. 이 때, shouldShowRequestPermissionRationale()을 체크하면, 해당 상태를 알 수 있고, 필요한 경우 App의 Settings 화면으로 사용자를 유도할 수도 있다. 앞에서 다룬 샘플 코드로 보면 다음 위치에서 확인할 수 있다는 얘기.
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
Log.i("Permission: ", "Granted")
} else {
Log.i("Permission: ", "Denied")
if(!shouldShowRequestPermissionRationale()) {
// PERMISSION DENIED AND NEVER SHOW AGAIN.
// '거부 및 다시 묻지 않음' 요기서 체크 가능.
}
}
}