권한 설정 하기

JuYong-Kim·2023년 6월 14일
0

Android 개발 기록

목록 보기
1/4
post-thumbnail

1. 시작

모든 안드로이드 앱은 접근이 제한된 샌드박스에서 실행된다. 즉, 앱이 샌드박스 밖에 리소스나 정보를 사용해야 하는 경우 권한을 선언 하고 해당 엑세스를 제공하는 권한을 요청해야 한다.

이전에는 앱이 시작되는 시점에 필요한 모든 권한을 한번에 요청했다. 하지만 보안 규정이 업데이트 되면서 각 권한이 필요한 기능이 실행(runtime)될 때 사용자에게 해당 권한이 필요한 이유를 밝히고 최소한의 권한을 획득하도록 변경되었다. 이를 Runtime Permission 이라고 한다.

해당 권한이 필요할 때 요청한다.
사용자에게 권한의 필요성을 충분히 설명한다.

문제는 런타임 권한 요청은 Acitivty 에 강한 의존성을 가지는 것에서 시작한다. 구글에서 SAA(Single Activity Architecture) 를 권장하고 있기 때문에 Fragment 에서 권한을 요청받아야 하는데 이때, Activity 를 거쳐야만 하는 문제가 발생한다.

심지어 공식 문서를 보면 알겠지만 권한 요청 및 처리 코드는 상당히 길고 복잡하다...(Ted Permission 같은 라이브러리가 있지만 최신 API 버전에서 제대로 동작하지 않음을 확인했다..)

그래서 권한 요청 기능을 하는 클래스를 분리하고 정리해서 다음 프로젝트에 사용하고자 한다.

2. 권한 선언

Manifest에 권한 추가

먼저 AndroidManifest.xml 파일에 권한이 필요하다는 것을 선언해야한다. android.permission. 안에 모든 권한이 정의되어 있으니 필요한 권한을 선언하면 된다.

<manifest ...>
    <uses-permission android:name="android.permission.CAMERA"/>
    <application ...>
        ...
    </application>
</manifest>

Hardware 선택사항 선언

CAMERA 같은 일부 권한은 기기 하드웨어에도 엑세스할 수 있도록 허용해야 한다. 이때, 해당 기기가 하드웨어가 있는지 없는지 여부를 먼저 판단해서 앱 실행 여부를 판단할 수 있다.

단, 대부분의 하드웨어는 선택 사항이므로 android:requiredfalse 로 설정해서 선택사항으로 선언하는 것이 권장된다.

이때, false 로 선언하지 않는다면 해당 앱이 실행되기 위해서는 해당 하드웨어가 필수라고 생각하고 선언된 하드웨어가 없다면 앱 설치할 수 없다.

<manifest ...>
    <application>
        ...
    </application>
    <uses-feature android:name="android.hardware.camera"
                  android:required="false" />
<manifest>

3. 권한 요청

앞서 말한 문제점들이 지속적으로 있어서 프로젝트 전체에 사용가능한 권한 요청 클래스를 구현하였다. MainActivity에 존재하던 것을 분리하여 구현한 것 이므로 주석과 함께 읽으면 이해가 어렵지 않을 것이다.

PermissionHelper.class 구현

라이브 데이터를 이용해서 결과값을 Fragment에서 바로 관찰하고 처리할 수 있도록 구현하였다.

import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData

private const val TAG = "PermissionHelper"

class PermissionHelper(private val activity: Activity) {

    private val permissionResultLiveData = MutableLiveData<PermissionResult>()
    private var REQUEST_CODE = -1

    data class PermissionResult(val permissions: List<String>, val grantResults: IntArray)

    // 다중 권한 여부 체크
    fun checkPermissions(permissions: Array<String>): List<String> {
        val deniedPermissions = permissions.filter {
            ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
        }
        return deniedPermissions
    }

    // 권한 요청
    fun requestPermissions(permissions: Array<String>, requestCode: Int) {
        val nonGrantedPermissions = checkPermissions(permissions)
        REQUEST_CODE = requestCode
        ActivityCompat.requestPermissions(
            activity,
            nonGrantedPermissions.toTypedArray(),
            requestCode
        )
    }

    // 권한 요청에 대한 결과 처리
    fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE) {
            permissionResultLiveData.value = PermissionResult(permissions.toList(), grantResults)
        }
    }

    // 권한 요청 결과 LiveData 반환
    fun getPermissionResultLiveData(): MutableLiveData<PermissionResult> {
        return permissionResultLiveData
    }

    // 권한 요청 결과 LiveData를 초기화
    fun clearPermissionResult() {
        permissionResultLiveData.value = null
    }
}

호출

이때, 주의 깊게 볼 것은 PermissionHelperonRequestPermissionsResult(...) 함수를 MainActivityoverride fun onRequestPermissionsResult(...) 안에서 호출하고 있다는 점이다.

안드로이드 시스템은 권한 승인 결과값을 Activity에게 전달해주므로, 결과값을 받은 Activity에서 PermissionHelper 에게 결과값 처리과정을 위임해줘야한다.

class MainActivity : AppCompatActivity() {

    private val permissionHelper by lazy { PermissionHelper(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        requestPermission()
    }

    private val permissions = arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_CALENDAR)
    private fun requestPermission() {
        // 권한 요청 결과 LiveData를 관찰
        permissionHelper.getPermissionResultLiveData().observe(this) { permissionResult ->
            Log.d(TAG, "requestPermission: $permissionResult")
            permissionResult?.let {
                if (it.grantResults.all { result -> result == PackageManager.PERMISSION_GRANTED }) {
                    // 권한이 모두 허용된 경우
                    // 권한에 대한 작업을 진행하세요.
                } else {
                    // 권한이 거부된 경우
                    // 거부에 대한 처리를 진행하세요.
                }
                // 권한 요청 결과 처리 후 LiveData 초기화
                permissionHelper.clearPermissionResult()
            }
        }

        if (permissionHelper.checkPermissions(permissions).isEmpty()) {
            // 모든 권한이 허용된 상태입니다.
            // 권한에 대한 작업을 진행하세요.
        } else {
            // 권한 요청
            permissionHelper.requestPermissions(permissions, PERMISSION_REQUEST_CODE)
        }
    }


    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        permissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    companion object {
        private const val PERMISSION_REQUEST_CODE = 101
    }
}

4. 마치며

항상 프로젝트를 할 때 소홀해지게 되는 것이 권한처리다. API 버전마다 그 차이가 심하고, 심지어 최근 API 버전인 API33 에서 변동사항이 있기 때문에 헤매기 쉽고, 안드로이드에서 권장하는 사항이 꽤나 많다.
지금 작성한 코드도 최대한 어떻게 사용하면 모듈화 할 수 있을지 고려하며 직접 작성한 것이라 보완할 점이 많다. 그래서 틈틈히 살펴보고 사용하면서 부족한 부분을 추가할 예정이다.

안드로이드 공식 문서 > 런타임 권한 요청

profile
Hello World!

0개의 댓글