Mobile : Android/Framework

[Android] 권한(Permission) # 1 - 시스템 권한 (feat. Bluetooth)

신숭이 2022. 5. 16. 01:03

[Android] 권한(Permission) # 1 - 시스템 권한 (feat. Bluetooth)

 

 

 

작성 시점 : 22.05.15

 

 

 

 

안드로이드 개발을 하다 보면, 특정 기능을 사용할 때 권한(Permission)이 필요한 경우가 있다. 특히 안드로이드 12 (API Level 31 이상)부터 몇 가지 권한의 변경 사항이 있어 현재 내가 개발 중인 앱도 컴파일 버전(CompileSdk)을 올려야 할 필요가 있었는데 그 영향이 있었다. 그래서 내친김에 권한(Permission)에 대해 정리해보고 기본적인 권한 요청 방식에 대해 알아보고자 한다.

 

 

 

1. 권한의 기본 개념

2. 권한의 종류

3. 권한 요청의 기본 로직

 

 

 

 

권한의 기본 개념

 

권한(Permission) 이란?

우선 권한이 무엇인지 간단히 살펴보고자 한다. 우리는 안드로이드의 여러 컴포넌트(Activity, Service 등)들이 각각 블록처럼 되어있고, 이것들을 조합해서 하나의 앱으로 구성된 다는 것을 알고 있다. 그러므로 어떤 경우에는 외부(다른 앱)에서 자신의 앱을 이용하거나 자신의 앱의 일부(컴포넌트)를 이용할 수 있다. 액티비티를 예시로 하면 우리가 안드로이드 스튜디오를 통해 액티비티를 처음 추가하면 다음과 같이 Manifest.xml 에 자동으로 추가되는 내용을 확인할 수 있다.

 

<activity
   android:name=".ota.OtaActivity"
   android:exported="false" >
</activity>

 

여기서 exported 속성을 true로 한다면, 이 액티비티의 정확한 클래스명을 아는 모든 앱에서 이 액티비티를 실행할 수 있다. (좀 더 범용성 있게 만드려면 <intent-filter> 태그를 이용하면 되지만 해당 포스트의 범위를 벗어난다)

 

이제 개발을 할 때 특정한 목적을 가지고, 우리 앱의 컴포넌트를 외부에서 접근 가능하게 하고자 했다고 하자. 그런데 모든 앱은 말고 우리 앱이 허락하는 앱만 접근 가능하게 하고자 할 때 권한(Permission) 설정이 필요하다. 권한 설정은 간단하게 Manifest.xml 에서 다음과 같이 설정하면 된다. 

 

name : 권한의 이름

label, descrition : 사용자가 보게 될 권한에 대한 설명

protection level : 보호 수준

 

<permission
    android:name="com.test.permission.TEST_PERMISSION"
    android:label="test permission"
    android:description="test permission"
    android:protectionLevel="normal" />

 

이렇게 하면 다음과 같이 <uses-permission> 을 Manifest.xml 에 선언해두지 않은 앱은 접근할 수 없다. 이게 안드로이드에서 다루는 권한(Permission)의 기본 개념이다.

 

<uses-permission android:name="com.test.permission.TEST_PERMISSION"/>

 

위와 같이 커스텀 권한이 아닌, 우리가 주로 사용하는 권한들은 시스템 권한으로 보통 아래와 같이 써오곤 했던 것을 기억할 것이다.

 

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

 

 

 

 

 

 

 

권한의 종류

 

 

 

위의 코드에서 protectionlevel 속성에 따라 권한을 구분한다.  위의 예시에서는 normal이라고 되어있는데 이는 낮은 수준의 접근 권한이다. 권한은 다음과 같이 구분되며, 앱의 보안과도 직결되는 것이기에 반드시 알고 넘어가는 것이 좋다. 다양한 종류가 있는데, 안드로이드 개발자 문서에서 설명하는 내용만 알아도 될 것 같다.

 

 

일반 권한 (protectionlevel = "normal")

낮은 수준의 보호로, 앱 사용자(User)에게 권한 부여 요청을 필요로 하지 않는다. 해당 권한으로 사용되는 기능은 데이터 작업이나 사용자의 개인 정보에 거의 영향을 미치지 않는다. ( 예 : android.permission.INTERNET ) 보통 설치 시간 권한이라 하여 앱을 설치할 때 자동으로 권한을 부여받는다.

 

 

서명 권한 (protectionlevel = "signature")

이 권한은 두 앱이 동일한 키로 서명되었을 경우 사용이 가능하다.  

 

 

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

높은 수준의 보호이다. 해당 권한으로 부여받는 기능은 시스템이나 다른 앱에 미치는 영향이 좀 더 크고 사용자의 개인 정보에 접근할 수도 있다. 따라서 이 수준의 권한을 사용하고자 할 때, 우리는 권한을 부여받는 로직을 설정할 필요가 있다. 

권한을 요청하는 다이얼로그

 

 

 

 

권한 요청의 기본 로직

 

 

권한 요청은 런타임 권한을 이용하고자 할때, 사용자에게 해당 권한 부여 요청을 하는 것을 뜻한다. 그냥 requestPermissions() 함수 호출하면 되는 것 아닌가 싶을 수 있는데, 생각보다 할 것들이 조금 있다. 일단 안드로이드 개발자 문서에서 권장하는 워크 플로우는 다음과 같은데, 참고만 하면 될 것 같다.

 

 

 

조금 간추려 보면 워크 플로우는 다음과 같다.

 

(적당한 UX 설계 후)

1. 권한을 부여받았는지 확인 -> 이미 부여받았다면 완료

ㄴ 2. 권한을 부여받지 않음 -> 권한 요청 -> 사용자 승인 시 완료

   ㄴ 3. 사용자 권한 요청 거절 시 -> (왜 필요한지 설명 후) 다시 권한 요청 -> 사용자 승인 시 완료

       ㄴ 4. 또 권한 요청 거절 시(;;) -> (왜 필요한지 설명 후) 권한 설정 페이지로 리디렉션 후 사용자 승인 유도

 

여기서 권한 요청을 사용자가 두 번 이상 거절한 순간부터, requestPermissions() 메서드의 결과가 무조건 권한 승인 실패로 디폴트 설정되기에 해당 함수를 이용할 수 없다. 이 때문에 권한 설정 페이지로 리디렉션이 필요하다. 왜 사용자가 두 번 이상 거절할까? 사실 왜 그렇게 까지 악착같이 거절하는지 의도는 모르겠지만 우리는 개발자니깐 모든 예외처리는 반드시 깔끔하게 해놓아야 한다. 억 단위 유저가 이용할 앱을 만들거면 다양한 예외가 나올 테니 이런 서비스를 만드는 게 꿈이라면 익숙해지자.

 

그리고 개발자 문서에서는 끝내 유저로 부터 권한을 부여받지 못했을 때, 해당 기능을 제외한 상태로라도 어느 정도 앱을 이용할 수 있게 설계하도록 권장하고 있다. 그렇게 따르도록 하자. 

 

예시는 블루투스 권한을 부여받는 것으로 하겠다. 안드로이드 12부터 블루투스 스캔 권한이 런타임 권한으로 상향되었다. 이 때문에 주변기기를 탐지하기 위해선 반드시 사용자(User)의 권한이 필요하다.

 

 

1. Manifest.xml에 필요한 권한들을 <uses-permission> 태그를 통해 선언한다.

    <uses-feature
        android:name="android.hardware.bluetooth"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />

    <uses-permission
        android:name="android.permission.BLUETOOTH"
        android:maxSdkVersion="30"/>
    <uses-permission
        android:name="android.permission.BLUETOOTH_ADMIN"
        android:maxSdkVersion="30"/>

    <!-- SDK 30 이하를 타겟팅 하는 앱 Bluetooth 권한 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- SDK 31 이상을 타겟팅 하는 앱 Bluetooth 권한 (Android 12+) -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

기본적인 블루투스, BLE 기능 (통신 및 스캔)을 사용하고자 할 때 필요한 권한들이다. 앱이 타겟팅하고 있는 SDK 버전에 따라 선언해야 할 권한이 차이가 있다.  안드로이드 12 이상 부터는 BLUETOOTH_SCAN, BLUETOOTH_CONNECT 권한이 반드시 필요하다.

 

 

 

2. 권한을 이미 부여받은 적 있는지  검사 (checkSelfPermission)

private fun bleInitialize(){
    // 런타임 권한 확인
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
                checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED ||
                checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                requestBlePermissions()
                return
            }
        }
        else {
            if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                requestBlePermissions()
                return
            }
        }

    .. 권한을 부여 받은 적 있으므로 이후 기능 수행

블루투스 기능을 이용하기 위해선 세 개의 런타임 권한이 필요하다. 해당 권한들을 부여받았는지 확인하기 위해 checkSelfPermission() 함수를 이용하도록 한다. 만약 부여받은 적이 없다면, 권한 요청을 수행한다. 타겟팅하는 SDK 버전에 따라 필요한 권한이 다르므로 다음과 같이 별도의 함수로 구성했다.

 

 

 

3. 권한을 부여받은 적이 없다면, 권한 요청 (requestPermission)

private var PERMISSION_REQUEST_CODE_S = 101
private var PERMISSION_REQUEST_CODE = 100
    
.. 중략


private fun requestBlePermissions(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            requestPermissions(
                arrayOf(
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT,
                    Manifest.permission.ACCESS_FINE_LOCATION
                ),PERMISSION_REQUEST_CODE_S)
        }
        else {
            requestPermissions(
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                PERMISSION_REQUEST_CODE)
        }
    }

 

여기서 requestPermissions() 함수를 이용한다. 여러개 권한 요청을 할 때 좋다. 여기서 요청 코드를 정해둬야 하는데, 사용자가 어떤 선택을 했는지 알기 위해선, 액티비티의 메서드 중 onRequestPermissionsResult() 함수를 오버라이드 해서 확인해야 하기 때문이다. 나는 여기서 SDK 31 이상은 PERMISSION_REQUEST_CODE_S로 두고, 31 미만에 대해선 PERMISSIOIN_REQUEST_CODE로 따로 정의하였다.  요청 시 다음과 같은 다이얼로그를 볼 수 있다.

 

 

 

 

4. 사용자의 선택 확인 (onRequestPermissionsResult)

@RequiresApi(Build.VERSION_CODES.S)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when(requestCode){
        PERMISSION_REQUEST_CODE_S -> {
            if(grantResults.isNotEmpty()
                && grantResults[0] == PackageManager.PERMISSION_GRANTED
                && grantResults[1] == PackageManager.PERMISSION_GRANTED
                && grantResults[2] == PackageManager.PERMISSION_GRANTED){
                .. 비즈니스 로직 수행
            }
            else{
                makePermissionSnackBar()
                App.setPreferenceDataBoolean("permission",false)
            }
        }
        PERMISSION_REQUEST_CODE -> {
            if(grantResults.isNotEmpty()
                && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                .. 비즈니스 로직 수행
            }
            else{
                makePermissionSnackBar()
                App.setPreferenceDataBoolean("permission",false)
            }
        }
    }
}

 

이제 각 SDK 버전에 따른 요청 응답 코드에 대응하자. 여기서 grantResult 배열이 있는데, 이 배열의 요소들은 내가 요청한 권한들과 1대1로 매칭 되는 것으로 해당 권한에 사용자가 승인을 선택하였다면 PERMISSION_GRANTED(0)가 저장되어있다. 이걸로 모두 승인되었음이 확인되면 다음 비즈니스 로직을 수행하면 될 것이다. 그런데 만약 사용자가 승인을 거절했다면 다음과 같이 수행한다. 먼저 사용자에게 다시 권한 승인을 요청하기 위해 간단한 메시지를 스낵바로 표시해 줬다. makePermissionSnackBar()라는 함수를 별도로 만들고 해당 함수를 호출한 후, PreferenceData로 Permission을 두 번 거절했는지 확인하기 위해 false를 먼저 저장한다.

 

 

5. 권한 요청 승인 "허용 안함" 시의 로직 구현

private fun makePermissionSnackBar(){
    val firstCheck = GosleepApp.getPreferenceDataBoolean("permission",true)
    val snackBar = Snackbar.make(binding.layout, R.string.permission_request, Snackbar.LENGTH_INDEFINITE)

    if(firstCheck) {
        snackBar.setAction("권한승인") { requestBlePermissions() }
        snackBar.apply {
            setBackgroundTint(ContextCompat.getColor(this@SleepActivity, R.color.blue_50))
            setTextColor(ContextCompat.getColor(this@SleepActivity, R.color.blue_10))
            setActionTextColor(ContextCompat.getColor(this@SleepActivity, R.color.white))
            show()
        }
    }
    else{
        snackBar.setAction("확인") {
            val intent = Intent()
            intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
            val uri = Uri.fromParts("package", packageName, null)
            intent.data = uri
            startActivity(intent)
        }
        snackBar.apply {
            setBackgroundTint(ContextCompat.getColor(this@SleepActivity, R.color.blue_50))
            setTextColor(ContextCompat.getColor(this@SleepActivity, R.color.blue_10))
            setActionTextColor(ContextCompat.getColor(this@SleepActivity, R.color.white))
            show()
        }
    }
}

첫 번째 거절 시
두 번째 거절 시

두 번째 거절인지 확인하기 위해 firstCheck 라는 Boolean 타입 변수로 확인을 하고 있고, 두 번째 거절이면 우리는 더 이상 requestPermissions 함수를 사용할 수 없게 된다. (안드로이드 규정? 상 무조건 PERMISSION_GRANTED(0)가 오지 않는다.) 이 때문에 사용자가 설정 창으로 넘어갈 수 있게 해야 한다. (이게 아니면 재설치밖에 답이 없다.)

snackBar.setAction("확인") {
    val intent = Intent()
    intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
    val uri = Uri.fromParts("package", packageName, null)
    intent.data = uri
    startActivity(intent)
}

여기서 유저는 권한을 누른 뒤 권한을 승인해야한다.

 

이 설정창으로 넘어가기전에 간단히 이미지로 어떻게 해야 하는지 보여주는 게 가장 좋을 것이다. 이건 디자이너에게 요청하도록 하자. 설정 창에서 사용자가 권한에 모두 승인을 하고 백버튼으로 우리 앱으로 돌아오면 액티비티 수명 주기 상 onStart()가 호출되므로 여기서 비즈니스 로직을 수행하도록 하면 될 것이다.

 

 

 

 

 

 

 

이러한 로직을 간편하게 제공하는 한국인이 만든 인기있는 라이브러리도 있다. (TedPermission)

https://github.com/ParkSangGwon/TedPermission

 

GitHub - ParkSangGwon/TedPermission: Easy check permission library for Android Marshmallow

Easy check permission library for Android Marshmallow - GitHub - ParkSangGwon/TedPermission: Easy check permission library for Android Marshmallow

github.com