본문 바로가기

Android

Android Runtime Permission

안드로이드 어플리케이션은 예전엔 AndroidManifest.xml 파일에 권한을 명시해주기만 하면 됐지만 Android 6.0(API 수준 23) 이상의 경우 안드로이드 정책에 따라 사용자가 직접 권한을 허용 해야 합니다. 

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

 

Test App에서 Target APP의 컴포넌트를 실행한다고 가정했을 때, Target App에서 해당 컴포넌트의 <permission> 을 부여했다면 testApp에서 해당 컴포넌트를 실행하기 위해선 반드시 <uses-permission> 을 선언해 사용해야 합니다.

 

[Android Runtime Permission 공식 문서]

 

런타임 권한 요청  |  Android 개발자  |  Android Developers

런타임 권한 요청 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 모든 Android 앱은 액세스가 제한된 샌드박스에서 실행됩니다. 앱이 자체 샌드박스 밖에 있

developer.android.com

Android Runtime permission WorkFlow Diagram

RunTime Permission Request WorkFlow

  • (1) Android Manifest.xml 파일에 Permission 선언
  • (2) Application UI/UX 설계
  • (3) 사용자의 요청을 대기 하다 필요한 Runtime Permission 요청
  • (4) Permission이 이미 허용 되었는지 확인
  • (5), (6) Permission이 거부됐을 경우 Permission 요청 / 필요할 경우 Permission이 필요한 이유를 설명
  • (7, 8a) Permission이 허용된 경우 권한에 보호되는 기능 사용 
  • (7, 8b) Permission이 거부된 경우 권한에 보호되는 기능 사용  불가 

위 Diagram은 다음과 같이 요약할 수 있다.

최초 권한 거절 -> 거절 당한 권한을 다시 요청 -> 거절과 동시에 해당 권한 요청을 다시 표시하지 않음

기본 원칙

  • 사용자가 권한이 필요한 기능을 사용하려 할 때 컨텍스트에 따라 권한을 요청 합니다.
  • Android에선 Permission에 대한 권한 요청은 사용자 사용성을 고려하여 두 번만 실행할 수 있습니다.
  • 사용자가 권한 요청을 두 번 이상 거부하면 영구 거부로 간주됩니다. 
  • 한번 거절된 Permission을 재요청할 때 해당 Permission이 필요한 이유를 사용자에게 노출 시킵니다
  • 또 거절된 경우엔 App을 종료시키지 않고 권한이 필요한 기능을 사용 중지하는 등의 방법으로 앱의 성능을 단계적으로 저하시켜 사용자가 앱을 계속 사용할 수 있도록 합니다.

권한 종류

1. 일반 권한(Protectionlevel = "nomal")

가장 낮은 수준의 보호 권한으로 사용자에게 권한 요청을 하지 않고 어플리케이션 설치 시 자동으로 권한을 부여받습니다.

ex) android.permission.INTERNET

 

2. 서명 권한 (protectionlevel = "signature", "signatureOrSystem")

  • signature : 동일한 키로 서명된 App만 실행하도록 허용하는 권한
  • signatureOrSystem : 안드로이드 System App이거나 동일 키로 서명된 App만 실행하도록 허용하는 권한.

3. 런타임 권한 (protectionlevel = "dangerous")

높은 수준의 보호 권한으로 민감한 개인정보에 접근하거나 다른 App에 영향을 줄 수 있는 권한에대해선 사용자에게 권한 부여 요청을 해야 합니다.

시스템 퍼미션

위의 링크를 클릭하면 시스템 퍼미션관련 정보가 나오는데 퍼미션 정보와 protectionLevel에 대한 설명이 나와있습니다.

 

시스템 퍼미션은 특정 기능을 OS에서 보호하고 있어 앱에서 사용할 때  <uses-permission> 을 선언 해줘야 합니다.

시스템의 퍼미션 중 이용빈도가 높은 퍼미션으로는 다음과 같습니다.

앱에 이미 권한이 부여되었는지 확인

ContextCompat.checkSelfPermission() 메서드로 사용자가 이미 앱에 특정 권한을 부여했는지 확인할 수 있습니다. 이 메서드는 앱에 권한 여부에 따라 PERMISSION_GRADTED 또는 PERMISSION_DENIED를 반환합니다.

val status = ContextCompat.checkSelfPermission(this, "android.permission.ACCESS_FINE_LOCATION")
if(status == PackageManager.PERMISSION_GRANTED) Log.d("permissionStatus", "Permission Granted")
else Log.d("permissionStatus", "Permission Denied")

앱에 권한이 필요한 이유 설명

requestPermissions()를 호출하면 앱에서 원하는 권한은 표시되지만 권한이 필요한 이유에 대해선 제시되지 않습니다.

위치, 마이크, 카메라와 관련된 권한은 앱에게 사용자에 관한 특히 민감한 정보에 액세스할 수 있는 권한을 부여합니다

Android 12(API 수준 31)부터 애플리케이션이 마이크나 카메라에 액세스할 때마다 개인 정보 보호 표시기가 사용자에게 알립니다.

Android 권한 요청 방법 : Android 11 (API 30, R os) 이전

 Android API 30 이전까진 ActivityCompat API의 requestPermissionsonRequestPermissionsResult를 통해 권한을 획득할 수 있었습니다. Android에선 명확하고 독립적인 권한 요청 Flow를 지원하기 위해 API 30 이상부터 이 방식을 통한 권한 요청은 deprecated되었습니다. 그 이유는 onRequestPermissionsResult 안에서 모든 권한 요청이 처리되는데, 이러한 방식은 권한 요청 Flow가 Activity 및 Fragment Lifecycle에 밀접하게 연관되어 있음에도 이에 대한 권한 요청을 처리하는 방법이 명확하지 않아 구현과 관리가 복잡하고 여러 권한을 함께 처리하데 제한적이기 때문입니다.

val status = ContextCompat.checkSelfPermission(this, "android.permission.ACCESS_FINE_LOCATION")
    if(status == PackageManager.PERMISSION_GRANTED) Log.d("permissionStatus", "Permission Granted")
    else {
        ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 909)
    }

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode){
        909 -> {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 권한 요청 허용 했을 경우의 logic 처리
            } else {
                // 권한 요청 거부했을 경우의 logic 처리
                // ex) permission 재요청 or 무시 등등
            }
        }
    }
}

Android 권한 요청 방법(2) : Android 11 (API 30, R os) 이후

ActivityResultLauncher를 사용해 사용자에게 퍼미션을 요청합니다. 이 클래스는 Activity에서 결과를 돌려받아야 할 때(CallBack) 사용하며 대표적으로 퍼미션 허용 요청과 다른 Activity를 실행하고 결과를 돌려받을 때 사용합니다.

이 함수는 두 개의 매개변수를 가지고 있습니다.

  • ActivityResultContract : 어떤 퍼미션에 대한 요청인가를 나타내며 다양한 요청에 대응하는 서브 클래스들이 있는데 다른 액티비티를 실행하고 결과를 돌려받는 StartActivityForResult, 퍼미션 허용 요청시 사용하는RequestPermission 등이 있습니다.
  • ActivityResultCallback : 결과값을 받았을 때 호출되는 콜백 함수입니다.
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
        @NonNull ActivityResultContract<I, O> contract,
        @NonNull ActivityResultCallback<O> callback) {
    return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

다중 퍼미션 허용 요청 확인

val multiplePermissionsLauncher = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        permissions.entries.forEach { (permission, isGranted) ->
            when {
                isGranted -> {
                    // 권한이 승인된 경우 처리할 작업
                }
                !isGranted -> {
                    // 권한이 거부된 경우 처리할 작업
                }
                else -> {
                    // 사용자가 "다시 묻지 않음"을 선택한 경우 처리할 작업
                }
            }
        }
        // multiple permission 처리에 대한 선택적 작업
        // - 모두 허용되었을 경우에 대한 code
        // - 허용되지 않은 Permission에 대한 재요청 code
    }

 

냉동 코더님의 블로그 내용을 바탕으로 안드로이드에서 서버로 이미지를 전송할 때의 권한 설정 연습을 해봤습니다.

https://cliearl.github.io/posts/android/request-runtime-permission/

 

Runtime Permission 이해하고 요청 구현하기

이번 포스팅에서는 런타임 퍼미션에 대해 알아보도록 하겠습니다. 들어가기 안드로이드는 카메라나 저장소, 위치 이용와 같이 시스템에 큰 영향을 미칠 수 있는 기능에 대해서는 위험한 권한으

cliearl.github.io

Android 11 (API 30, R os) 이전

1. 인터넷과 내부 저장소(갤러리)를 사용할 수 있게 AndroidMiafest,xml 파일에 권한을 선언해 줬습니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

 

2. 요청하는 권한은 한 묶음으로 만들어 다루기 위해 REQUIRED_PERMISSIONS로 정의합니다.

companion object{
    private val REQUIRED_PERMISSIONS = arrayOf(
        Manifest.permission.INTERNET,
        Manifest.permission.READ_EXTERNAL_STORAGE,
    )
}

 

3. 권한을 확인 함수를 작성합니다. ContextCompat.checkSelfPermission으로 해당 권한이 부여되었는지 확인합니다. REQUIRED_PERMISSIONS에 대해 all을 수행하여 이중 하나라도 권한이 부여되지 않은 경우 isAllPermissionsGranted가 false를 반환하도록 했습니다.

private fun isAllPermissionsGranted(): Boolean = REQUIRED_PERMISSIONS.all { permission->
    ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
}

 

4. 버튼을 클릭 했을 때 권한이 허용되지 않았다면 권한을 요청하고 그 결과는 onRequestPermissionsResult CallBack으로 반데 됩니다.  onRequestPermissionsResult에서는 requestCode를 확인한 뒤 권한이 취득되지 않았을 경우 shouldShowRequestPermissionRationale를 사용해 권한을 재 요청할 수 있고 그럼에도 요청을 거부하면 앱은 더이상 권한을 요청할 수 없습니다. 

binding.pbtn.setOnClickListener{
        if(isAllPermissionsGranted()){
            Snackbar.make(binding.root, "Permission granted", Snackbar.LENGTH_SHORT).show()
        } else {
            requestDangerousPermissions()
        }

    }
}

private fun requestDangerousPermissions() {
    ActivityCompat.requestPermissions(
        this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQUEST_CODE_PERMISSIONS) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 모든 권한을 취득함
            Snackbar.make(binding.root, "Permission granted", Snackbar.LENGTH_SHORT).show()
        } else {
            if (shouldShowRequestPermissionRationale(REQUIRED_PERMISSIONS[0])) {
                // 권한을 다시 요구함
                Snackbar.make(binding.root,
                    "Permission required to use app!", Snackbar.LENGTH_SHORT).show()
                requestDangerousPermissions()
            } else {
                // 앱설정 화면으로 들어가서 설정할 수 있도록 함
                Snackbar.make(binding.root, "Permission denied", Snackbar.LENGTH_SHORT).show()
//                        openSettings()
            }
        }
    }
}

private fun openSettings() {
    Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        data = Uri.fromParts("package", packageName, null)
    }.run(::SettingActivity)
}

Android 11 (API 30, R os) 이후

ActivityResultContracts.RequestMultiplePermissions()으로 다중 권한을 요청하고 허용되지 않은 권한이 있다면
shouldShowRequestPermissionRationale()에 필요한 permission을 파라미터로 전달해 권한을 재요청 합니다.

private val requestPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        permissions.entries.forEach { permission ->
            when {
                permission.value -> {
                    Snackbar.make(binding.root, "Permission granted", Snackbar.LENGTH_SHORT).show()
                }
                shouldShowRequestPermissionRationale(permission.key) -> {
                    Snackbar.make(binding.root,
                        "Permission required to use app!", Snackbar.LENGTH_SHORT).show()
                }
                else -> Snackbar.make(binding.root, "Permission denied", Snackbar.LENGTH_SHORT).show()
            }
        }
    }
    
binding.locationIcon.setOnClickListener {
    if (isAllPermissionsGranted()) {
        Snackbar.make(binding.root, "Permission granted", Snackbar.LENGTH_SHORT).show()
    } else {
        requestPermissionLauncher.launch(REQUIRED_PERMISSIONS)
    }
}

 

'Android' 카테고리의 다른 글

Anroid REST API 연동기 [GET]  (3) 2023.12.22
Android Gallery Permission  (0) 2023.12.17
OkHTTP httpLoggingInterceptor  (0) 2023.12.15
Retrofit Network Time out  (0) 2023.12.15
안드로이드 HTTP 보안 정책  (0) 2023.12.14