기기의 현재위치나 내부저장소 파일처럼 민감한 정보에 접근할 권한은 구글에서 따로 '위험한 권한' 혹은 '런타임 권한'으로 분류하고 있다. 안드로이드 6 (API level 23) 부터 개발자는 위험한 권한을 AndroidManifest.xml에 명시하는 것에 추가로 런타임에서 직접 유저에게 권한 부여를 요청해야 한다. 앱을 다운받고 처음 켰을 때 뜨는 아래와 같은 모양의 다이얼로그가 바로 이것이다.
API레벨마다 위험한 권한의 종류도 다르고 요청 코드도 복잡하여 상당히 짜증을 유발하는데 이번 기회에 한번 정리해 보려 한다.
권한명 | 용도 | 추가 API레벨 | 비고 |
---|---|---|---|
ACCEPT_HANDOVER | 다른 앱에서 건 통화를 계속 수신 (ex.영상통화) | 28 | |
ACCESS_BACKGROUND_LOCATION | 백그라운드에서 위치정보 접근 | 29 | ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION 필요 |
ACCESS_COARSE_LOCATION | 기지국 위치를 이용해 대략적인 위치정보 접근 | 1 | |
ACCESS_FINE_LOCATION | 기기의 정확한 위치정보 접근 | 1 | |
ACCESS_MEDIA_LOCATION | 유저 파일에 기록된 위치정보 접근 (ex. 사진의 EXIF데이터) | 29 | |
ACTIVITY_RECOGNITION | 유저의 움직임 상태 정보 접근 | 29 | |
BODY_SENSORS | 인바디 센서 정보 접근 | 20 | |
BODY_SENSORS_BACKGROUND | 백그라운드에서 인바디 센서 정보 접근 | 33 | BODY_SENSORS 필요 |
ADD_VOICEMAIL | Allows an application to add voicemails into the system. | 14 | |
ANSWER_PHONE_CALLS | 통화 자동응답 | 26 | |
BLUETOOTH_ADVERTISE | 다른 블루투스 기기에서 내 기기 검색 | 31 | |
BLUETOOTH_CONNECT | 페어링된 다른 블루투스 기기에 연결 | 31 | |
BLUETOOTH_SCAN | 다른 블루투스 기기 검색 | 31 | |
CALL_PHONE | 전화 앱 거치지 않고 통화 기능 사용 | 1 | |
CAMERA | 기기 카메라 사용 | 1 | |
GET_ACCOUNTS | Accounts Service 사용 | 1 | API 23부터는 거의 불필요 (공식문서 참조) |
NEARBY_WIFI_DEVICES | 다른 Wi-Fi기기 검색 및 내 Wi-Fi기기 검색 허용 | 33 | |
POST_NOTIFICATIONS | 알림 표시 | 33 | |
PROCESS_OUTGOING_CALLS | 발신 통화 중 상대방 번호에 접근 | 1 | API 29부터 deprecated |
권한명 | 용도 | 추가 API레벨 | 비고 |
---|---|---|---|
READ_CALENDAR | 유저 캘린더 읽기 | 1 | |
READ_CALL_LOG | 통화 이력 읽기 | 16 | |
READ_CONTACTS | 유저 주소록 읽기 | 1 | |
READ_EXTERNAL_STORAGE | 외부저장소 읽기 | 16 | API 19부터 강제화, WRITE_EXTERNAL_STORAGE 취득시 자동 취득, API 33부터 불필요 (READ_MEDIA_AUDIO, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO 로 대체) |
READ_MEDIA_AUDIO | 외부저장소 오디오파일 읽기 | 33 | READ_EXTERNAL_STORAGE 대체 |
READ_MEDIA_IMAGES | 외부저장소 이미지파일 읽기 | 33 | READ_EXTERNAL_STORAGE 대체 |
READ_MEDIA_VIDEO | 외부저장소 비디오파일 읽기 | 33 | READ_EXTERNAL_STORAGE 대체 |
READ_PHONE_NUMBERS | 기기 전화번호 읽기 | 26 | |
READ_PHONE_STATE | 기기 상태 (네트워크 정보 등) 읽기 | 1 |
권한명 | 용도 | 추가 API레벨 | 비고 |
---|---|---|---|
WRITE_CALENDAR | 유저 캘린더 쓰기 | 1 | |
WRITE_CALL_LOG | 통화 이력 쓰기 | 16 | |
WRITE_CONTACTS | 유저 주소록 쓰기 | 1 | |
WRITE_EXTERNAL_STORAGE | 외부저장소 쓰기 | 4 | API 30부터 효력 상실. 아래 참고 |
권한명 | 용도 | 추가 API레벨 | 비고 |
---|---|---|---|
READ_SMS | SMS메시지 읽기 | 1 | |
RECEIVE_MMS | MMS메시지 받기 | 1 | |
RECEIVE_SMS | SMS메시지 받기 | 1 | |
RECEIVE_WAP_PUSH | WAP푸쉬 허용 | 1 | |
RECORD_AUDIO | 녹음 기능 사용 | 1 | |
SEND_SMS | SMS메시지 보내기 | 1 | |
USE_SIP | SIP서비스 사용 | 9 | |
UWB_RANGING | Required to be able to range to devices using ultra-wideband. | 31 |
권한 요청은 액티비티에서 진행하는데, 권한 요청 결과를 2가지 콜백 메소드로 받을 수 있다. 하나는 액티비티가 가진 onRequestPermissionResult() 메소드를 오버라이딩하는 것이고, 다른 하나는 registerForAcitivityResult() 메소드를 호출하는 것이다. 두 방식은 request code 처리법에서 차이가 있는데, 구글에서 전자를 deprecate시켰기 때문에 장기적으로 후자를 쓰는 것이 유리할 것으로 생각된다. 공식 문서에 두 방식 모두 설명되어 있다.
권한 요청 프로세스는 아래의 순서로 진행된다.
아래의 코딩 예시는 1, 3, 5를 생략한 것이다. API 레벨에 따라 요청하는 권한이 달라지는 케이스를 보여주기 위해 WRITE_EXTERNAL_STORAGE 권한으로 예시를 잡았다.
class MainActivity : AppCompatActivity() {
val PERMISSION_REQUEST_CODE = 1234 //임의의 정수 지정
// 6. API로부터 권한 요청 결과를 콜백으로 수신
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
// 7. 결과에 따라 후속 작업 진행
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
if (
grantResults.isNotEmpty()
&& (grantResults[0] == PackageManager.PERMISSION_GRANTED)
) {
Toast.makeText(this, "권한 승인", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "권한 거부", Toast.LENGTH_SHORT).show()
}
return
}
else -> {
// Ignore all other requests.
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
val wes = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
// 2. 런타임에서 권한 부여 상태 확인
if (VERSION.SDK_INT < 29) {
if (checkSelfPermission(wes) != GRANTED){
// 4. 권한 요청 API 작동
requestPermissions(
this,
arrayOf(wes),
PERMISSION_REQUEST_CODE
)
}
}
}
}
class MainActivity : AppCompatActivity() {
// 6. API로부터 권한 요청 결과를 콜백으로 수신
// 4보다 먼저 초기화되어야 함
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
// 7. 결과에 따라 후속 작업 진행
if(permissions.entries.all { it.value }){
Toast.makeText(this, "권한 승인", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "권한 거부", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
val wes = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
// 2. 런타임에서 권한 부여 상태 확인
if (VERSION.SDK_INT < 29) {
if (checkSelfPermission(wes) != GRANTED){
// 4. 권한 요청 API 작동
permissionLauncher.launch(arrayOf(wes))
}
}
}
}