Permission 중에 Runtime permission은 앱이 사용자에게 요청을 하고 사용자가 허용해야 받을 수 퍼미션을 말합니다.
그렇기 때문에 어떤 퍼미션이 필요한 API를 사용하려면 사용자에게 꼭 요청을 해야합니다. 사용자가 요청을 거절할 수도 있기 때문에 이에 대한 예외처리도 필요합니다.
Runtime permission은 SDK API 23(Mashmello) 부터 지원됩니다. SDK API 23 미만의 버전으로 컴파일했다면 앱이 설치될 때 퍼미션을 획득할 수 있습니다. 따라서 API 23 미만은 Runtime permission을 요청할 필요가 없습니다.
앱이 설치될 때 얻는 퍼미션은 Install permission이라고 합니다.
퍼미션 요청 API
Activity가 AppCompatActivity를 상속하면 다음과 같은 API를 사용할 수 있습니다.
- checkSelfPermission()
- requestPermissions()
- shouldShowRequestPermissionRationale()
이 API들을 사용하여 사용자에게 퍼미션을 요청할 수 있습니다.
또한, ActivityCompat으로 동일한 동작을 수행하는 API를 사용할 수 있습니다.
- ActivityCompat.checkSelfPermission()
- ContextCompat.checkSelfPermission()
- ActivityCompat.requestPermissions()
- ActivityCompat.shouldShowRequestPermissionRationale()
이제 위의 API들을 사용하여 Runtime permission을 사용자에게 요청하고 얻는 방법을 알아보겠습니다.
프로젝트 설정
이 글의 코드는 Kotlin으로 작성되었습니다.
프로젝트의 Target SDK API는 23 버전 이상을 사용하도록 합니다.
완성된 프로젝트는 GitHub - RequestRuntimePermission에서 확인할 수 있습니다.
퍼미션을 갖고 있는지 확인
checkSelfPermission()
으로 앱이 퍼미션을 갖고 있는지 확인을 할 수 있습니다.
다음과 같이 확인하고 싶은 퍼미션을 인자로 전달하면 PERMISSION_GRANTED
또는 PERMISSION_DENIED
가 리턴됩니다.
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
// Permission is granted
} else {
// Permission is not granted
}
PERMISSION_GRANTED
가 리턴되었다면 퍼미션을 갖고 있다는 것이고, 그렇지 않다면 퍼미션이 없다는 의미입니다.
퍼미션을 갖고 있다면 어떤 기능이 수행되도록 구현할 수 있습니다.
사용자에게 퍼미션 요청
requestPermissions()
으로 사용자에게 Permission을 요청할 수 있습니다.
다음과 같이 먼저 checkSelfPermission()
으로 Permission을 갖고 있는지 확인하고 없다면 requestPermissions()
으로 요청합니다.
companion object {
const val PERMISSION_REQUEST_CODE = 1001
}
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
showDialog("Permission granted")
} else {
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), // 1
PERMISSION_REQUEST_CODE) // 2
}
//1
과 같이 위의 주석으로 표시한 것을 아래에서 자세히 설명하였습니다.
- 퍼미션을 배열로 전달하는 이유는, 이 API는 여러 퍼미션을 동시에 요청할 수 있도록 만들어졌기 때문입니다. 만약 한개의 퍼미션만 요청한다고 해도 배열로 전달해야 합니다.
PERMISSION_REQUEST_CODE
는 단순한 상수인데, 권한 요청에 대한 응답으로 이벤트가 전달될 때 이 code로 전달됩니다. 숫자는 아무거나 해도 되는데, 다른 이벤트들과 구분되어야 합니다.
퍼미션 요청 결과 받기
requestPermissions()
으로 사용자에게 퍼미션을 요청하면 다음과 같은 팝업이 뜹니다.
사용자가 allow를 누르거나 deny를 누르면 그 결과가 내 앱으로 전달됩니다.
Framework는 결과를 전달할 때 Activity의 onRequestPermissionsResult()
를 호출합니다.
다음은 이벤트를 받을 때 처리하는 코드입니다.
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
PERMISSION_REQUEST_CODE -> { // 1
if (grantResults.isEmpty()) { // 2
throw RuntimeException("Empty permission result")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 3
showDialog("Permission granted")
} else {
if (shouldShowRequestPermissionRationale(
Manifest.permission.ACCESS_FINE_LOCATION)) { // 4
Log.d(TAG, "User declined, but i can still ask for more")
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
PERMISSION_REQUEST_CODE)
} else {
Log.d(TAG, "User declined and i can't ask")
showDialogToGetPermission() // 5
}
}
}
}
}
private fun showDialogToGetPermission() {
val builder = AlertDialog.Builder(this)
builder.setTitle("Permisisons request")
.setMessage("We need the location permission for some reason. " +
"You need to move on Settings to grant some permissions")
builder.setPositiveButton("OK") { dialogInterface, i ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) // 6
}
builder.setNegativeButton("Later") { dialogInterface, i ->
// ignore
}
val dialog = builder.create()
dialog.show()
}
//1
과 같이 위의 주석으로 표시한 것을 아래에서 자세히 설명하였습니다.
- 요청할 때 사용한 code로 이벤트가 Activity로 전달됩니다.
- grantResults는 empty가 될 수 없습니다. empty라면 무시하면 됩니다.
- 여러 Permission을 요청할 수 있기 때문에 그 결과도 배열로 전달됩니다. 여기서는 1개의 퍼미션을 요청했기 때문에 Index 0의 결과만 확인하면 됩니다. 퍼미션을 받았다면 어떤 기능이 수행되도록 구현할 수 있습니다. 여기서는 팝업을 띄우도록 하였습니다.
shouldShowRequestPermissionRationale()
는 사용자가 Deny 버튼을 눌렀을 때 true를 리턴합니다. 사용자가 Deny & Don't ask again 버튼을 누르면 false를 리턴합니다. true가 리턴되는 경우 사용자에게 다시 권한을 요청할 수 있습니다.- 하지만 false가 리턴되는 경우
requestPermissions
를 호출해도 요청 팝업이 뜨지 않습니다. 퍼미션을 받으려면 사용자는 Settings의 내 앱 정보에 들어가서 퍼미션을 변경해야 합니다. 따라서, 왜 퍼미션이 필요한지 충분히 설명하는 팝업을 띄우고 OK 버튼을 누르면 Settings로 이동할 수 있도록 구현하였습니다. ACTION_APPLICATION_DETAILS_SETTINGS
인텐트를 실행하면 내 앱의 설정화면으로 이동합니다.
예제 실행 결과
requestPermissions()
으로 사용자에게 퍼미션을 요청하면 다음과 같은 팝업이 뜹니다.
만약 Deny 를 누르면 요청 팝업이 반복적으로 뜹니다.
Deny & Don't ask again 를 누른다면 다음과 같이 앱이 퍼미션을 꼭 얻어야하는 이유를 설명하는 팝업이 뜹니다.
여기서 사용자는 내 앱에게 퍼미션을 부여할 수 있습니다.
Fragment에서 퍼미션 요청
Fragment에서 퍼미션을 요청하거나, Helper class에서 퍼미션을 요청하도록 구현할 수 있습니다.
Fragment도 대부분 동일한 API를 호출할 수 있지만, 호출할 수 없는 경우에 ActivityCompat
를 이용하여 호출하면 됩니다.
이 API를 호출할 때 인자로 activity를 전달해주어야 합니다.
다음은 Fragment에서 퍼미션을 요청하는 코드입니다.
private fun requestRuntimePermissions() {
if (ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
// Permission granted
} else {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
MainActivity.PERMISSION_REQUEST_CODE
)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
grantResults: IntArray) {
when (requestCode) {
MainActivity.PERMISSION_REQUEST_CODE -> {
if (grantResults.isEmpty()) {
throw RuntimeException("Empty permission result")
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
requireActivity(),
Manifest.permission.ACCESS_FINE_LOCATION)) {
Log.d(MainActivity.TAG, "User declined, but i can still ask for more")
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
MainActivity.PERMISSION_REQUEST_CODE
)
} else {
Log.d(MainActivity.TAG, "User declined and i can't ask")
}
}
}
}
}
만약 퍼미션을 요청하는 Helper 클래스를 만든다면,
onRequestPermissionsResult()
는 호출되지 않기 때문에 Activity나 Fragment로부터 callback되도록 구현해야 합니다.
전체 프로젝트 코드
프로젝트의 모든 코드는 GitHub - RequestRuntimePermission에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력