본문 바로가기

Android

Android Permission 요청하기

Permission의 종류

안드로이드에는 두 종류의 권한이 있습니다.

 

첫째는 사용자에게 따로 권한 승인 여부를 묻지 않고, 설치 시간에 권한을 부여받을 수 있는 설치 시간 권한(Install-time permissions)입니다.

The left image shows a list of an app's install-time permissions. The     right image shows a pop-up dialog that contains 2 options: allow and deny.

위 종류에 해당하는 권한은 Manifest에 선언하기만 하면, 앱 설치와 동시에 사용할 수 있는 권한들입니다.

 

 

두번째는, 런타임 권한(Runtime-permission)입니다.

A pop-up dialog that contains 2 options: allow and deny.

 

위와 같은 다이얼로그를 통해 사용자에게 직접 권한 승인을 요청하여 권한을 부여받습니다. 주로 개인정보에 접근할 수 있는 권한을 런타임 퍼미션으로 분류하고 있습니다.

 

안드로이드 퍼미션의 종류는 Permission list에서 확인할 수 있습니다.

 

런타임 퍼미션

마시멜로우 버전(API 23) 이상 버전부터는 반드시 런타임 퍼미션(Runtime-permissions)을 이용해서 권한을 부여받아야합니다.

img

  1. 매니페스트 파일에 필요한 권한 선언하기
  2. 권한이 필요한 이유를 사용자에게 알리기 위해 UI 표현하기
  3. 승인 요청 후 사용자의 승인을 기다리기
  4. 권한이 이미 승인되어 있는 상태일 경우

5-a. 사용자에게 권한 요청을 재질의하기

5-b. 사용자에게 권한이 필요한 이유를 더 자세히 설명하기

  1. 권한 요청을 하기 위한 다이어로그 실행하기
  2. 사용자로부터 권한을 승인 받았습니까?

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) 내에서 권한을 요청하고 승인 혹은 거절 결과를 직접 수신할 수 있게 되었다.

이제 스크린에서 기능을 수행 중에 권한 문제로 액티비티로 로직이 전환되었다가 다시 프래그먼트로 돌아오는 일 없이 권한 승인을 받을 수 있다.