Permission의 종류
안드로이드에는 두 종류의 권한이 있습니다.
첫째는 사용자에게 따로 권한 승인 여부를 묻지 않고, 설치 시간에 권한을 부여받을 수 있는 설치 시간 권한(Install-time permissions)입니다.
위 종류에 해당하는 권한은 Manifest에 선언하기만 하면, 앱 설치와 동시에 사용할 수 있는 권한들입니다.
두번째는, 런타임 권한(Runtime-permission)입니다.
위와 같은 다이얼로그를 통해 사용자에게 직접 권한 승인을 요청하여 권한을 부여받습니다. 주로 개인정보에 접근할 수 있는 권한을 런타임 퍼미션으로 분류하고 있습니다.
안드로이드 퍼미션의 종류는 Permission list에서 확인할 수 있습니다.
런타임 퍼미션
마시멜로우 버전(API 23) 이상 버전부터는 반드시 런타임 퍼미션(Runtime-permissions)을 이용해서 권한을 부여받아야합니다.
- 매니페스트 파일에 필요한 권한 선언하기
- 권한이 필요한 이유를 사용자에게 알리기 위해 UI 표현하기
- 승인 요청 후 사용자의 승인을 기다리기
- 권한이 이미 승인되어 있는 상태일 경우
5-a. 사용자에게 권한 요청을 재질의하기
5-b. 사용자에게 권한이 필요한 이유를 더 자세히 설명하기
- 권한 요청을 하기 위한 다이어로그 실행하기
- 사용자로부터 권한을 승인 받았습니까?
8-a. 부여받은 권한을 사용합니다.
8-b. 권한을 부여받지 못했으므로, 권한이 없을 때의 동작을 이어나갑니다.
권한을 요청할 때 사용하는 메소드
- 권한을 가지고 있는지 체크하는 메소드
- ActivityCompat.checkSelfPermission(Context, String)
- 권한을 요청하는 메소드
- ActivityCompat.requestPermissions(Activity, String[], int)
- 권한 요청 결과를 확인받는 콜백 메소드
- ActivityCompat.OnRequestPermissionsResultCallback
즉, 안드로이드의 권한 요청은 AcitivtyCompat에 강한 의존성을 지니고 있습니다.
이전의 권한 획득은 앱이 시작되는 시점에 추후 앱에서 필요한 권한을 한꺼번에 요청하여 부여받았습니다.
하지만 보안관련 규정이 업데이트되면서, 각 권한이 필요한 기능이 실행될 때(runtime) 사용자에게 기능동작을 위한 해당 권한의 필요성을 적극적으로 알리고 최소한의 권한을 획득하도록 고안된 방법입니다.
- 예를 들어, 카카오톡에서 일반적인 메시지를 주고 받을 때는 파일접근 / 카메라의 권한을 승인하지 않아도 사용가능해야합니다.
- 하지만 카카오톡에 사진 전송하려면, 앨범에 있는 사진을 선택(파일접근)하거나 새로운 사진을 촬영(카메라)에 접근할 수 있는 권한을 카카오톡에 승인해야만 기능이 작동할 수 있습니다.
키포인트는
- 해당 권한이 정말 필요할때 요청한다.
- 미래의 필요성을 대비해 미리 권한을 요청하지 않는다.
- 사용자에게 권한의 필요성을 충분히 설명한다.
- 사용자 몰래 뒤에서 권한을 부여받을 수 없도록 강제한다. (강력한 보안)
문제점
런타임 퍼미션의 문제점은 Activity와 강한 의존성을 갖는다는 점입니다.
최근 구글은 SigleActivity - Multiple Fragments 구조를 권장하고 있으며, 여러 개의 프래그먼트로 화면을 구성하는 것은 굉장히 보편적인 일입니다.
하지만 각 Fragment에서 권한 요청을 하기 위해서는 반드시 Activity의 손을 거쳐야만 합니다.
게다가, 권한을 부여받지 못하면 해당 기능이 동작하지 않기 때문에 사용자가 1회 승인 거절했을 경우에 한번 더 승인을 요청하는 프로세스를 가지고 있습니다.
- Permission Granted
- Permission Denied
- Permission Denied and Do not ask again
3가지의 상태를 가지고 있고, 여러 개의 다른 퍼미션을 요청하게되면 코드가 굉장히 복잡해지게 됩니다.
다음은 공식 문서의 런타임 퍼미션 요청 예제입니다.
when {
// 권한을 가지고 있는지 체크
ContextCompat.checkSelfPermission(
CONTEXT,
Manifest.permission.REQUESTED_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
performAction(...)
}
// 1회 승인 거부 시, 다시 승인이 필요한 이유를 자세히 설명하기.
shouldShowRequestPermissionRationale(...) -> {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected. In this UI,
// include a "cancel" or "no thanks" button that allows the user to
// continue using your app without granting the permission.
showInContextUI(...)
}
else -> {
// You can directly ask for the permission.
requestPermissions(CONTEXT,
arrayOf(Manifest.permission.REQUESTED_PERMISSION),
REQUEST_CODE)
}
}
그리고 권한을 수신하는 액티비티의 메소드는
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
// If request is cancelled, the result arrays are empty.
if ((grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
// Permission is granted. Continue the action or workflow
// in your app.
} else {
// Explain to the user that the feature is unavailable because
// the features requires a permission that the user has denied.
// At the same time, respect the user's decision. Don't link to
// system settings in an effort to convince the user to change
// their decision.
}
return
}
// Add other 'when' lines to check for other
// permissions this app might request.
else -> {
// Ignore all other requests.
}
}
}
굉장히 복잡합니다. 그리고 서로 다른 화면에서 필요한 기능이 액티비티 안에서 섞여서 존재할 수 밖에 없습니다.
권한요청 기능을 분리하기
분리된 클래스에서 권한 요청 기능을 담당할 수 있다면,
액티비티나 프래그먼트에서 권한 요청에 대한 결과값에 대한 코드를 일일히 집어 넣지 않고, 권한기능을 담당 클래스를 호출하여 사용할 수 있을 것 입니다.
권한 요청을 위해서는 크게 4가지의 스텝을 가집니다.
- manifest에 필요한 권한 선언하기
- 권한이 승인되어 있는지 체크하기
ContextCompat.checkSelfPermission(Manifest.permission.REQUESTED_PERMISSION)
- Permission의 구분은
Manifest.permission.REQUESTED_PERMISSION
으로 String 값입니다.
- 권한 요청하기
- 사용자의 승인여부 결과값 수신하기
먼저 Permission 값을 요청할 때마다 Maniefest.permission.*의 긴 이름을 입력하는 것은 매우 번거롭습니다.
만약 {FINE_LOCATION, CAMERA, RECEIVE_SMS} 3가지 권한을 요청한다면,
- android.permission.FINE_LOCATION
- android.permission.CAMERA
- android.permission.RECEIVE_SMS
가독성이 떨어지는 긴 이름을 반복적으로 입력해야만 합니다.
그래서 sealed class를 이용해서 앱 내에서 사용할 권한을 선언하고, 앱에서는 sealed class의 멤버를 사용해서 권한을 요청하도록 만듭니다.
sealed class PermissionMember(
private val androidPermission: String
) {
companion object {
// 위의 긴 String 권한이름을 PermissionMember로 매핑하는 함수입니다.
fun fromAndroidPermission(requestPermission: String): PermissionMember {
// android.permission.FineLocation -> PermissionMember.FineLocation
}
}
// PermissionMember를 String 권한 이름으로 매핑하는 함수입니다.
fun getAndroidPermission(): String = androidPermission
// 앱 내부에서는 권한요청할 때 아래 멤버들을 사용합니다.
object FineLocation: PermissionMember("android.permission.FINE_LOCATION")
object Camera: PermissionMember("android.permission.CAMERA")
object ReceiveSms: PermissionMember("android.permission.RECEIVE_SMS")
}
그리고 권한 요청을 담당하는 클래스를 생성합니다.
class PermissionRequester @Inject constructor(
private val activity: Activity
) : Observable<PermissionRequester.Listener>() {
interface Listener {
fun onRequestPermissionResult(requestCode: Int, result: PermissionResult)
fun onPermissionRequestCancelled(reqestCode: Int)
}
fun hasPermission(permission: PermissionMember): Boolean {
retrun ContextCompat.checkSelfPermission(activity, permission.getAndroidPermission()) == PackageManager.PERMISSION_GRANTED
}
fun requestPermission(permission: PermissionMember, requestCode: Int) {
ActivityCompat.requestPermissions(activity, arrayOf(permission.getAndroidPermission()), requestCode)
}
// 권한 요청에 대한 결과값 처리
fun onRequestPermissionResult(requestCode: Int, androidPermissions: Array<out String>, grantResults: IntArray) {
// 문제는 권한승인 결과는 Android system에 의해서 Activity#onRequestPermissionResult() 메소드에 전달됩니다.
}
}
그래서 Activity가 수신하는 권한승인 결과정보를 다시 PermissionRequester에게 delegation해주어야만 합니다.
// MainActivity.kt
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
// This method call must be launched here to delegate permission granted results To [PermissionRequester].
permissionRequester.onRequestPermissionResult(requestCode, permissions, grantResults)
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
- PermissionRequester는 권한요청을 담당하는 클래스로 옵저버블하며, 결과값을 콜백 Listener로 반환한다.
- 각 권한 요청이 필요한 Fragment는 위 Listener를 구현하여 결과값을 수신하며, PermissionRequester를 리스닝(옵저빙)한다.
- 하지만, 안드로이드 시스템은 권한 승인 결과값을 Activity에게 전달해주므로, 결과값을 받은 Activity는 PermissionRequester에게 결과값 처리과정을 위임해줘야한다. (*시스템 구조상 Activity의 일부 종속성을 제거할 수 없음)
- 각 Fragment 내에서 PermissionRequester를 통해 권한 요청을 하고, 결과를 직접 수신할 수 있다.
- 그리고 각 권한은 sealed class로 표현되어, when을 사용해서 else 없이 모든 케이스에 대해 대응코드를 표현할 수 있다.
실행결과
AirQualityFragment에서 결과값을 수신하는 모습을 확인할 수 있습니다.
Conclusion
런타임 퍼미션 요청을 간단하기 위해서 독립된 클래스 PermisionRequester를 만들고, 이를 이용해서 각 스크린(Fragment) 내에서 권한을 요청하고 승인 혹은 거절 결과를 직접 수신할 수 있게 되었다.
이제 스크린에서 기능을 수행 중에 권한 문제로 액티비티로 로직이 전환되었다가 다시 프래그먼트로 돌아오는 일 없이 권한 승인을 받을 수 있다.
'Android' 카테고리의 다른 글
클린 아키텍처와 데이터 모델들 (0) | 2021.12.11 |
---|---|
SearchView에서 Soft Keyboard 보여주기 (0) | 2021.12.03 |
UI Component 분리시키기 (0) | 2021.12.02 |
NavigationDrawer와 Toolbar 연동하기 (0) | 2021.12.02 |
Toolbar와 ActionBar의 차이점 (0) | 2021.12.02 |