# Table Of Contents

# 권한

안드로이드 앱에서 사용자의 자원에 접근하려면 핸드폰 소유자에게 권한(Permission)을 부여받아야 합니다. 권한에는 크게 두 종류가 있습니다.

# 일반 권한

일반 권한AndroidManifest.xml에 추가하기만 하면 됩니다. 사용자의 개인 정보에 접근하지 않는 권한이며, 대표적인 일반 권한은 아래와 같습니다.

<!-- 인터넷 사용 권한 -->
<uses-permission android:name="android.permission.INTERNET"/>

<!-- 네트워크 연결상태 확인 권한 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<!-- 와이파이 상태 확인 권한-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>

<!-- 블루투스 상태 확인 권한 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>

<!-- 근거리 통신 사용 권한 -->
<uses-permission android:name="android.permission.NFC"/>

<!-- 알람 설정 권한 -->
<uses-permission android:name="android.permission.SET_ALARM"/>

<!-- 진동 설정 권한 -->
<uses-permission android:name="android.permission.VIBRATE"/>

다음은 버튼을 눌렀을 때 Retrofit2를 사용하여 서버에 http 요청을 하는 코드입니다.

interface PostService {
    @GET("/post/posts")
    fun getPosts(@Query("page") page: Int,@Query("size") size: Int): Call<ResponseBody>
}
class MainActivity: AppCompatActivity() {
    
    val button: Button by lazy { findViewById<Button>(R.id.activity_main_button) }

    val page = 10
    var size = 10

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

        button.setOnClickListener {

            val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(authInterceptor)
            .build()

            val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build()

            val postService = retrofit.create(PostService::class.java)

            postService.getPosts(page, size).enqueue(object:  Callback<ResponseBody> {

                override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
                    // 성공했을 때    
                }
                override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
                    // 실패했을 때
                }
            })
        }
    }
}

만약 인터넷 사용 권한을 추가하지 않으면 다음과 같은 오류가 발생합니다.

java.lang.SecurityException: Permission denied (missing INTERNET permission?)

따라서 AndroidManifest.xml에 다음과 같이 권한을 명시해야합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yologger.example_android_permission">

    <!-- 인터넷 사용 권한 추가 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        <!-- ... -->
    </application>

</manifest>

# 위험 권한

위험 권한(Dangerous Permission)AndroidManifest.xml에 권한을 추가해야할 뿐만 아니라, 런타임에 권한 확인 및 권한 요청 코드를 반드시 작성해야합니다. 이 때문에 위험 권한은 런타임 권한이라고도 불립니다. 보통 앱을 처음 실행하거나 권한을 필요로 하는 기능을 사용할 때 권한을 확인합니다.

대표적인 위험 권한은 아래와 같습니다.

<!-- 달력 -->
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>

<!-- 카메라 -->
<uses-permission android:name="android.permission.CAMERA"/>

<!-- 전화번호부 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>

<!-- 위치 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION_LOCATION"/>

<!-- 마이크 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

<!-- 전화 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MODIFY_PHONE_NUMBER"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.ADD_VOICEMAIL"/>
<uses-permission android:name="android.permission.USE_SIP"/>

<!-- 센서 -->
<uses-permission android:name="android.permission.BODY_SENSORS"/>

<!-- 문자 메시지 -->
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>

<!-- 스토리지 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

우선 예제를 살펴봅시다. 예제는 버튼을 누르면 카메라를 실행합니다.

우선 AndroidManifest.xml에 카메라 권한을 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yologger.example_android_permission">

    <!-- 카메라 접근 권한 명시 -->
    <uses-permission android:name="android.permission.CAMERA"/>

    <application>
        <!-- ... -->
    </application>

</manifest>

버튼을 눌렀을 때, 카메라를 실행하는 openCamera()메소드를 호출합니다.

class MainActivity : AppCompatActivity() {

    private val buttonOpenCamera: Button by lazy {
        findViewById<Button>(R.id.button_open_camera)
    }

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

        buttonOpenCamera.setOnClickListener {
            openCamera()
        }
    }

    // 카메라 열기
    private fun openCamera() {
        var intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        startActivity(intent)
    }
}

위 예제를 실행하면 아래와 같이 카메라 접근 권한이 없다는 에러를 발생시킵니다.

java.lang.SecurityException: Permission Denial: starting Intent from ProcessRecord with revoked permission android.permission.CAMERA

따라서 우리는 권한이 있는지 확인하고, 권한이 없으면 권한을 요청하며, 사용자가 권한을 허용하거나 거절했을 때 어떻게 할지 코드로 작성해야합니다.

class MainActivity : AppCompatActivity() {

    private val REQUEST_CODE_CAMERA_PERMISSION = 1000

    private val buttonOpenCamera: Button by lazy {
        findViewById<Button>(R.id.button_open_camera)
    }

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

        buttonOpenCamera.setOnClickListener {
            // openCamera()
            checkPermissionAndOpenCamera()
        }
    }

    private fun checkPermissionAndOpenCamera() {
        // 권한 확인하기
        when(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)) {
            // 권한이 있을 때
            PackageManager.PERMISSION_GRANTED -> {
                // 카메라 열기
                openCamera()
            }
            // 권한이 없을 때
            PackageManager.PERMISSION_DENIED -> {
                // 권한 요청하기
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_CAMERA_PERMISSION)
            }
        }
    }

    private fun openCamera() {
        var intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        startActivity(intent)
    }
}

권한이 있는지 확인할 때는 ContextCompat.checkSelfPermission()메소드를 사용합니다. 권한이 없을 때 ActivityCompat.requestPermissions()메소드를 호출하면 다음과 같이 권한 요청 대화상자가 출력됩니다.

세 가지 선택지 중에 어떤 것을 선택해도 onRequestPermissionsResult()메소드가 자동으로 호출됩니다. 이 안에서 앱 사용 중에만 허용, 이번만 허용, 거부에 따라 다르게 처리해주면 됩니다.

class MainActivity : AppCompatActivity() {

    private val REQUEST_CODE_CAMERA_PERMISSION = 1000

    // ...

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when(requestCode) {
            REQUEST_CODE_CAMERA_PERMISSION -> {
                // 권한을 승인했을 경우
                if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // 카메라 열기
                    openCamera()
                // 권한을 승인하지 않았을 경우
                } else {
                    // 앱 종료
                    finish()
                }
            }
            else -> {
                // ...
            }
        }
    }
}

# 여러 권한 동시에 요청하기

우선 AndroidManifest.xml카메라 접근 권한달력 읽기 권한을 명시합니다.

// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.yologger.multi_permissions">

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

    <!-- ... -->

</manifest>

이제 MainActivity가 실행되면 권한을 확인하겠습니다. 다음과 같이 반복문을 활용하여 권한 여러 개를 확인할 수 있습니다.

class MainActivity : AppCompatActivity() {

    val REQUEST_CODE_PERMISSIONS = 1

    val button: Button by lazy { findViewById<Button>(R.id.activity_main_button) }

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

    private fun checkPermissions() {
        // 필요한 권한
        val requiredPermissions = arrayOf<String>(
            Manifest.permission.CAMERA,
            Manifest.permission.READ_CALENDAR
        )

        // 거절된 권한
        val rejectedPermissions = arrayListOf<String>()

        // 반복문 안에서 권한 확인
        for (permission in requiredPermissions) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                // 거절된 권한은 ArrayList에 추가
                rejectedPermissions.add(permission)
            }
        }

        if (rejectedPermissions.isNotEmpty()) {
            val array = arrayOfNulls<String>(rejectedPermissions.size)
            // 거절된 권한은 권한 요청
            ActivityCompat.requestPermissions(this, rejectedPermissions.toArray(array), REQUEST_CODE_PERMISSIONS)
        }
    }
}

이제 onRequestPermissionsResult()메소드를 정의합시다.

class MainActivity : AppCompatActivity() {

    val REQUEST_CODE_PERMISSIONS = 1

    // ...

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            REQUEST_CODE_PERMISSIONS -> {
                if(grantResults.isNotEmpty()) {
                    for ((i, permission) in permissions.withIndex()) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            // Do something when allowed.
                        } else {
                            // Do something when denied.
                        }
                    }
                }
            }
            else -> {
                // ...
            }
        }
    }
}

# 설정 앱

설정(Settings) 앱에서는 안드로이드와 관련된 환경 설정을 할 수 있습니다.

설정 > 어플리케이션에서는 설치된 어플리케이션을 확인할 수 있습니다.

특정 앱을 선택하고 권한을 클릭하면 앱이 필요로 하는 권한을 확인할 수 있습니다.

ALLOWED 항목에는 허용된 권한이 포함됩니다.

DENIED 항목에는 거부된 권한이 포함됩니다.

DENIED 항목에 포함된 권한을 선택하면 권한을 부여할 수 있습니다.

# 액티비티에서 설정 앱 시작하기

인텐트(Intent)를 사용하면 액티비티에서 설정 앱을 바로 시작할 수 있습니다. 버튼을 누르면 설정 앱을 바로 시작해보겠습니다.

class MainActivity : AppCompatActivity() {

    private val buttonOpenSettings: Button by lazy { findViewById<Button>(R.id.activity_main_btn_open_settings) }
    private val REQUEST_CODE = 0

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

        buttonOpenSettings.setOnClickListener {
            val intent = Intent(android.provider.Settings.ACTION_SETTINGS)
            startActivityForResult(intent, REQUEST_CODE)
        }
    }
}

설정 > 위치로 이동할 수도 있습니다.

val intent = Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivityForResult(intent, REQUEST_CODE)

설정 > 앱으로 이동할 수도 있습니다.

val intent = Intent(android.provider.Settings.ACTION_APPLICATION_SETTINGS)
startActivityForResult(intent, REQUEST_CODE)

설정 > 앱 > 특정 앱으로 이동할 수도 있습니다. 이 때는 인텐트에 패키지 이름을 추가해야합니다.

val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:${packageName}")
startActivityForResult(intent, REQUEST_CODE)

# TedPermission 라이브러리

TedPermission (opens new window)는 대한민국의 개발자 박상권님께서 개발하신 권한 설정 라이브러리입니다. 이 라이브러리를 사용하면 권한 설정을 좀 더 쉽게 구현할 수 있습니다.

# 설치

모듈 수준의 build.gradle에 의존성을 추가합니다.

dependencies {
    // ...
    implementation 'gun0912.ted:tedpermission:2.2.0'
}

# 사용 방법

예제에서 카메라 권한을 확인하고, 권한이 없으면 권한을 요청하고, 권한을 얻었을 때 카메라 앱을 구동하겠습니다.

이제 동일한 내용을 TedPermission 라이브러리를 사용해서 구현해보겠습니다.

class MainActivity : AppCompatActivity() {

    private val buttonOpenCamera: Button by lazy {
        findViewById<Button>(R.id.button_open_camera)
    }

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

        buttonOpenCamera.setOnClickListener {
            TedPermission.with(this)
                // 요청할 권한 나열하기
                .setPermissions(Manifest.permission.CAMERA)
                // 권한 요청 결과에 대한 리스너 등록
                .setPermissionListener(object: PermissionListener {
                    // 권한을 승인했을 경우
                    override fun onPermissionGranted() {
                        // 카메라 열기
                        openCamera()
                    }

                    // 권한을 승인하지 않았을 경우
                    override fun(deniedPermissions: ArrayList<String>?) {
                        // 앱 종료
                        finish()
                    }
                })
                // 권한을 거부할 경우 나타나는 Alert dialog의 메시지
                .setDeniedMessage("If you reject permission, you can not use this service. \n\nPlease turn on permissions at [Setting] > [Permission]")
                // 권한 요청하기
                .check()
        }
    }

    private fun openCamera() {
        var intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        startActivity(intent)
    }
}

이제 앱을 실행하고 버튼을 누르면 다음과 같이 권한 설정 대화상자가 나옵니다.

앱 사용 중에만 허용을 누르면 onPermissionGranted()메소드가 호출되며, 앱을 종료하고 다시 시작해도 권한 설정 대화상자가 나오지 않습니다. 이번만 허용을 눌러도 onPermissionGranted() 메소드가 호출됩니다. 차이점은 앱 종료 후 다시 시작할 때마다 권한 설정 대화상자가 다시 나오느냐 입니다.

만약 거부를 누르면 setDeniedMessage()메소드에서 설정한 메시지와 함께 설정 또는 닫기를 선택할 수 있는 대화상자가 나옵니다.

여기서도 닫기를 누르면 onPermissionDenied()가 호출됩니다. 만약 설정을 누르면 다음과 같이 앱 설정 화면으로 이동합니다.

여기서 권한을 모두 부여하고 뒤로가기를 누르면 onPermissionGranted()이 호출됩니다. 그러나 하나라도 권한을 부여하지 않으면 onPermissionDenied()이 호출됩니다.

# 다중 권한 요청

여러 권한이 필요할 때 다음과 같이 동시에 요청할 수도 있습니다.

TedPermission.with(this)
    // 권한 여러개 설정
    .setPermissions(Manifest.permission.CAMERA, Manifest.permission.READ_CALENDAR)
    .setPermissionListener(object: PermissionListener {
        override fun onPermissionGranted() {
            // ...
        }
        override fun(deniedPermissions: ArrayList<String>?) {
            // ...
        }
    })
    .setDeniedMessage("If you reject permission, you can not use this service. \n\nPlease turn on permissions at [Setting] > [Permission]")
    .check()