Android - Runtime permission 요청 방법 (kotlin)

JS · 31 May 2020

Runtime permission은 앱이 사용자에게 요청을 하고 사용자가 허용해야 받을 수 퍼미션을 말합니다.

그렇기 때문에 어떤 퍼미션이 필요한 API를 사용하려면 사용자에게 꼭 요청을 해야합니다. 사용자가 요청을 거절할 수도 있기 때문에 이에 대한 예외처리도 필요합니다.

Runtime permission은 SDK API 23(Mashmello) 부터 지원됩니다. SDK API 23 미만의 버전으로 컴파일했다면 앱이 설치될 때 퍼미션을 획득할 수 있습니다. 따라서 API 23 미만은 Runtime permission을 요청할 필요가 없습니다.

앱이 설치될 때 얻는 퍼미션은 Install permission이라고 합니다.

퍼미션 요청 API

Activity가 AppCompatActivity를 상속하면 다음과 같은 API를 사용할 수 있습니다.

  • checkSelfPermission()
  • requestPermissions()
  • shouldShowRequestPermissionRationale()

이 API들을 사용하여 사용자에게 퍼미션을 요청할 수 있습니다.

또한, ActivityCompat으로 동일한 동작을 수행하는 API를 사용할 수 있습니다.

  • ActivityCompat.checkSelfPermission()
  • ContextCompat.checkSelfPermission()
  • ActivityCompat.requestPermissions()
  • ActivityCompat.shouldShowRequestPermissionRationale()

이제 위의 API들을 사용하여 Runtime permission을 사용자에게 요청하고 얻는 방법을 알아보겠습니다.

프로젝트 설정

이 글의 코드는 Kotlin으로 작성되었습니다.

프로젝트의 Target SDK API는 23 버전 이상을 사용하도록 합니다.

완성된 프로젝트는 GitHub - RequestRuntimePermission에서 확인할 수 있습니다.

퍼미션을 갖고 있는지 확인

checkSelfPermission()으로 앱이 퍼미션을 갖고 있는지 확인을 할 수 있습니다.

다음과 같이 확인하고 싶은 퍼미션을 인자로 전달하면 PERMISSION_GRANTED 또는 PERMISSION_DENIED가 리턴됩니다.

if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED) {
    // Permission is granted
} else {
    // Permission is not granted
}

PERMISSION_GRANTED가 리턴되었다면 퍼미션을 갖고 있다는 것이고, 그렇지 않다면 퍼미션이 없다는 의미입니다.

퍼미션을 갖고 있다면 어떤 기능이 수행되도록 구현할 수 있습니다.

사용자에게 퍼미션 요청

requestPermissions()으로 사용자에게 Permission을 요청할 수 있습니다.

다음과 같이 먼저 checkSelfPermission()으로 Permission을 갖고 있는지 확인하고 없다면 requestPermissions()으로 요청합니다.

companion object {
    const val PERMISSION_REQUEST_CODE = 1001
}

if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
        == PackageManager.PERMISSION_GRANTED) {
    showDialog("Permission granted")
} else {
    requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), // 1
            PERMISSION_REQUEST_CODE) // 2
}

//1과 같이 위의 주석으로 표시한 것을 아래에서 자세히 설명하였습니다.

  1. 퍼미션을 배열로 전달하는 이유는, 이 API는 여러 퍼미션을 동시에 요청할 수 있도록 만들어졌기 때문입니다. 만약 한개의 퍼미션만 요청한다고 해도 배열로 전달해야 합니다.
  2. PERMISSION_REQUEST_CODE는 단순한 상수인데, 권한 요청에 대한 응답으로 이벤트가 전달될 때 이 code로 전달됩니다. 숫자는 아무거나 해도 되는데, 다른 이벤트들과 구분되어야 합니다.

퍼미션 요청 결과 받기

requestPermissions()으로 사용자에게 퍼미션을 요청하면 다음과 같은 팝업이 뜹니다.

Android runtime permission popup

사용자가 allow를 누르거나 deny를 누르면 그 결과가 내 앱으로 전달됩니다.

Framework는 결과를 전달할 때 Activity의 onRequestPermissionsResult()를 호출합니다.

다음은 이벤트를 받을 때 처리하는 코드입니다.

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

    when (requestCode) {
        PERMISSION_REQUEST_CODE -> {  // 1
            if (grantResults.isEmpty()) {  // 2
                throw RuntimeException("Empty permission result")
            }
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {  // 3
                showDialog("Permission granted")
            } else {
                if (shouldShowRequestPermissionRationale(   
                          Manifest.permission.ACCESS_FINE_LOCATION)) { // 4
                    Log.d(TAG, "User declined, but i can still ask for more")
                    requestPermissions(
                            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                            PERMISSION_REQUEST_CODE)
                } else {
                    Log.d(TAG, "User declined and i can't ask")
                    showDialogToGetPermission()   // 5
                }
            }
        }
    }
}

private fun showDialogToGetPermission() {
    val builder = AlertDialog.Builder(this)
    builder.setTitle("Permisisons request")
            .setMessage("We need the location permission for some reason. " +
                    "You need to move on Settings to grant some permissions")

    builder.setPositiveButton("OK") { dialogInterface, i ->
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                Uri.fromParts("package", packageName, null))
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        startActivity(intent)   // 6
    }
    builder.setNegativeButton("Later") { dialogInterface, i ->
        // ignore
    }
    val dialog = builder.create()
    dialog.show()
}

//1과 같이 위의 주석으로 표시한 것을 아래에서 자세히 설명하였습니다.

  1. 요청할 때 사용한 code로 이벤트가 Activity로 전달됩니다.
  2. grantResults는 empty가 될 수 없습니다. empty라면 무시하면 됩니다.
  3. 여러 Permission을 요청할 수 있기 때문에 그 결과도 배열로 전달됩니다. 여기서는 1개의 퍼미션을 요청했기 때문에 Index 0의 결과만 확인하면 됩니다. 퍼미션을 받았다면 어떤 기능이 수행되도록 구현할 수 있습니다. 여기서는 팝업을 띄우도록 하였습니다.
  4. shouldShowRequestPermissionRationale()는 사용자가 Deny 버튼을 눌렀을 때 true를 리턴합니다. 사용자가 Deny & Don't ask again 버튼을 누르면 false를 리턴합니다. true가 리턴되는 경우 사용자에게 다시 권한을 요청할 수 있습니다.
  5. 하지만 false가 리턴되는 경우 requestPermissions를 호출해도 요청 팝업이 뜨지 않습니다. 퍼미션을 받으려면 사용자는 Settings의 내 앱 정보에 들어가서 퍼미션을 변경해야 합니다. 따라서, 왜 퍼미션이 필요한지 충분히 설명하는 팝업을 띄우고 OK 버튼을 누르면 Settings로 이동할 수 있도록 구현하였습니다.
  6. ACTION_APPLICATION_DETAILS_SETTINGS 인텐트를 실행하면 내 앱의 설정화면으로 이동합니다.

예제 실행 결과

requestPermissions()으로 사용자에게 퍼미션을 요청하면 다음과 같은 팝업이 뜹니다. Android runtime permission popup

Allow 를 누르면 다음과 같은 팝업이 뜹니다. Android runtime permission popup

만약 Deny 를 누르면 요청 팝업이 반복적으로 뜹니다.

Deny & Don't ask again 를 누른다면 다음과 같이 앱이 퍼미션을 꼭 얻어야하는 이유를 설명하는 팝업이 뜹니다. Android runtime permission popup

OK 를 누르면 내 앱의 설정화면으로 이동합니다. Android runtime permission popup

여기서 사용자는 내 앱에게 퍼미션을 부여할 수 있습니다. Android runtime permission popup

Fragment에서 퍼미션 요청

Fragment에서 퍼미션을 요청하거나, Helper class에서 퍼미션을 요청하도록 구현할 수 있습니다.

Fragment도 대부분 동일한 API를 호출할 수 있지만, 호출할 수 없는 경우에 ActivityCompat를 이용하여 호출하면 됩니다. 이 API를 호출할 때 인자로 activity를 전달해주어야 합니다.

다음은 Fragment에서 퍼미션을 요청하는 코드입니다.

private fun requestRuntimePermissions() {
    if (ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION)
            == PackageManager.PERMISSION_GRANTED) {
        // Permission granted
    } else {
        ActivityCompat.requestPermissions(
            requireActivity(),
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            MainActivity.PERMISSION_REQUEST_CODE
        )
    }
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
                                        grantResults: IntArray) {
    when (requestCode) {
        MainActivity.PERMISSION_REQUEST_CODE -> {
            if (grantResults.isEmpty()) {
                throw RuntimeException("Empty permission result")
            }
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission granted
            } else {
                if (ActivityCompat.shouldShowRequestPermissionRationale(
                        requireActivity(),
                        Manifest.permission.ACCESS_FINE_LOCATION)) {
                    Log.d(MainActivity.TAG, "User declined, but i can still ask for more")
                    ActivityCompat.requestPermissions(
                        requireActivity(),
                        arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                        MainActivity.PERMISSION_REQUEST_CODE
                    )
                } else {
                    Log.d(MainActivity.TAG, "User declined and i can't ask")
                }
            }
        }
    }
}

만약 퍼미션을 요청하는 Helper 클래스를 만든다면, onRequestPermissionsResult()는 호출되지 않기 때문에 Activity나 Fragment로부터 callback되도록 구현해야 합니다.

전체 프로젝트 코드

프로젝트의 모든 코드는 GitHub - RequestRuntimePermission에서 확인할 수 있습니다.

참고

codechachaCopyright ©2019 codechacha