안드로이드에서는 위치, 외부 저장소 등의 접근을 위해 사용자에게 시스템 다이얼로그 직접 접근 권한 허가를 받아야 한다.
최근 Android 13부터는 알람까지 접근 권한을 받아야 활성화되는 것으로 변경되었다.
하지만 사용자들이 매번 허용한다고 보장할 수 없다..
접근 권한 허가 요청을 2번 이상 거부 당하거나 과거 OS 버전처럼 다시 보지 않음을 체크하여 거부할 경우,
사용자가 특정 기능 사용을 영구적으로 제한받을 수 있다.
실제로 위와 같은 이유로 특정 기능이 안 된다는 사용자 리포트가 들어와 재설치를 안내하고, 권한 허가를 요청드린 경우가 있었는데,
이런 비용 낭비 시나리오를 대응하기 위해 영구적으로 권한 접근을 거절당하더라도 안내 Dialog를 통해 사용자가 직접 권한 설정을 변경할 수 있는 로직을 구성해 보기로 했다.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PermisstionCode -> if (grantResults.isNotEmpty()) {
var passAllPermission = true
for (i in grantResults.indices) { //요청한 권한들이 허가되었는지 검사
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
passAllPermission = false
}
}
if (passAllPermission) {
//TODO: 권한 허가 완료 이후 처리
} else {
val isRefusedPermanently = !shouldShowRequestPermissionRationale(permissions.first())
showRefusedPermissionDialog(this, isRefusedPermanently, "권한 허가", "권한 허가가 필요합니다.")
}
}
}
}
권한 허가를 요청한 뒤 onRequestPermissionResult에서 요청한 권한들이 모두 허가 충족되었는지 확인하여 passAllPermission이라는 변수에 결과를 담았다.
shouldShowRequestPermissionRationale(@NonNull String permission) 함수는 권한이 영구적으로 거부된 상태인지 확인 가능한 안드로이드 자체 제공 함수이다.
해당 값을 통해 권한 허가 요청 UI를 팝업 할 수 있는 조건인지 판단하는데, 이 값이 false로 UI를 띄우지 않는 상황이라면 권한 허가 요청이 영구적 거부된 상태인 것이다.
/**
* Gets whether you should show UI with rationale before requesting a permission.
*
* @param permission A permission your app wants to request.
* @return Whether you should show permission rationale UI.
*
* @see #checkSelfPermission(String)
* @see #requestPermissions(String[], int)
* @see #onRequestPermissionsResult(int, String[], int[])
*/
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
return getPackageManager().shouldShowRequestPermissionRationale(permission);
}
이제 실제로 구현한 showRefusedPermissionDialog() 함수를 살펴보자
필자는 일반 거부 상황에서도 안내 다이얼로그로 경고할 수 있도록 처리했다.
fun showRefusedPermissionDialog(context: Context?, isRefusedPermanently: Boolean, title: String, subTitle: String, onChecked: (() -> Unit)? = null) {
if (isRefusedPermanently) {
AlertDialog.Builder(context)
.setIcon(R.drawable.app_icon)
.setTitle(title)
.setMessage(subTitle)
.setPositiveButton(R.string.setup_now) { _, _ ->
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:${context?.packageName}"))
context?.startActivity(intent)
} catch (e: ActivityNotFoundException) {
val intent = Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)
context?.startActivity(intent)
}
onChecked?.invoke()
}
.setNegativeButton(R.string.later) { dialog, _ ->
onChecked?.invoke()
dialog?.dismiss()
}
.create()
.show()
} else {
AlertDialog.Builder(context)
.setIcon(R.drawable.app_icon)
.setTitle(title)
.setMessage(subTitle)
.setPositiveButton(R.string.confirm) { dialog, _ ->
onChecked?.invoke()
dialog?.dismiss()
}
.create()
.show()
}
}
시스템 접근 관련 사항이라 흐름에 맞게 시스템 기본 AlertDialog를 팝업 하도록 했다.
AlertDialog.Builder(context)
App 아이콘과 안내 문구 정도를 적용하여 isRefusedPermanently가 true면 바로 애플리케이션 설정 화면으로 이동할 수 있도록 처리했다. 간혹 디바이스 특성에 따라 Settings.ACTION_APPLICATION_DETAILS_SETTINGS가 확인되지 않는 경우도 있으니 ActivityNotFoundException을 고려하여 ACTION_MANAGE_APPLICATIONS_SETTINGS에 대한 대응 처리도 추가했다.
if (isRefusedPermanently) {
AlertDialog.Builder(context)
.setIcon(R.drawable.app_icon)
.setTitle(title)
.setMessage(subTitle)
.setPositiveButton(R.string.setup_now) { _, _ ->
try {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:${context?.packageName}"))
context?.startActivity(intent)
} catch (e: ActivityNotFoundException) {
val intent = Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)
context?.startActivity(intent)
}
onChecked?.invoke()
}
onChecked라는 콜백을 구성하여 다이얼로그 버튼 클릭에 대한 외부 처리를 추가할 수도 있다.