반응형

안드로이드 권한(Permission)은 시스템의 보안과 개인정보 보호를 위해 존재합니다.

 

이 권한은 Protection level로 구분된 일반 권한(Normal permissions)위험 권한(Dangerous permissions)으로 나눠집니다.

 

이것 외에도 시스템 앱(Preload 앱)에 사용될 수 있는 Protection level 종류가 더 있지만, 일반 앱 개발자의 경우 사용할 수 있는 것은 위의 두 가지 Protection level에 해당하는 권한들만 사용할 수 있습니다.

 

Protection level은 아래 링크를 참조하시길 바랍니다.

https://developer.android.com/reference/android/R.attr#protectionLevel

 

[일반 권한 / 위험 권한]

일반 권한

 

시스템이나 개인정보 보안 등의 위험을 초래하지 않기 때문에 앱에서 권한을 요청만 하면 자동으로 권한을 얻게 되어서 접근할 수 있습니다.

 

▶ 일반 권한 확인 방법

[Play 스토어에서 앱을 설치 시 확인 방법]

일반 권한만 존재하는 앱은 아래 왼쪽 그림과 같이 바로 다운로드 되면서 설치됩니다. (Android 5.1 이하 단말)

Play 스토어의 해당 앱 설명 하단에 보면 "앱 권한" 이 있는데 확인을 하면 아래 오른쪽 그림과 같이 나옵니다. (모든 단말)

 - 아래의 앱("또 다른 지식의 성전" 게임)에서는 "일반 권한"만 존재합니다.

 

 

[설치된 앱에서 확인 방법]

"앱 정보" 진입(설정-애플리케이션-해당 앱)후 "권한" 확인 시 아래 왼쪽 그림과 같이 "위험 권한"은 존재하지 않고, 빨간색으로 표시된 "옵션 더 보기"를 누르면 아래 오른쪽 그림과 같이 "일반 권한"이 존재하는 것을 확인할 수 있습니다.

 

 

위 앱의 네트워크 접근 권한은 인터넷 권한(android.permission.INTERNET)으로 레퍼런스에 확인 시 "일반 권한"임을 알 수 있습니다.

 


 

위험 권한

 

시스템이나 개인정보 보안 등의 위험을 초래할 수 있기 때문에 사용자가 앱이 요청한 권한을 확인하고 승인해야 비로소 앱에서 접근할 수 있습니다.

앱이 카메라, 블루투스, 지문인식, 네트워크, NFC 등 시스템의 장치를 사용하거나 메시지, 전화번호, 일정과 같은 민감한 사용자 정보를 이용하기 위해서는 사용자에게 권한 승인을 얻은 이후 접근할 수 있습니다.

앱은 사용자가 승인하지 않으면 카메라를 사용할 수 없고, 전화번호를 읽어 올 수도 없습니다.

 

▶ 위험 권한 확인 방법

[Play 스토어에서 앱을 설치 시 확인 방법]

위험 권한이 포함된 아래("Send Anywhere" 앱)의 경우 설치 시 사용자에게 권한에 대한 승인을 받는 창이 나옵니다.

(단말이 Android 5.1 이하인 경우 또는 단말이 Android 6.0 이상이지만 앱의 targetSdkVersion이 22 이하인 경우)

Android 6.0 이상의 단말에서 targetSdkVersion이 23 이상이면 "위험 권한"이 존재하더라도 권한 승인 창이 나오지 않고 바로 설치됩니다. 이것은 나중에 "런타임 권한 확인"이 가능하기 때문에 그렇게 동작하는 것입니다.

 

[설치된 앱에서 확인 방법]

앱 정보 진입(설정-애플리케이션-해당 앱)후 "권한" 확인 시 아래 왼쪽과 같이 앱에서 사용하는 "위험 권한" 목록이 나오고 여기에서 권한 설정 및 해제도 가능합니다. 아래 오른쪽은 앱을 최초 실행했을 때 "위험 권한" 사용 요청을 위한 런타임 권한 요청하는 것을 확인할 수 있습니다.

 

 

 

[런타임 권한 (Runtime permission)]  

Android 6.0에서 최초 도입이 되었습니다. 이는 앱 설치 시 권한 요청을 하는 것이 아니라, 앱 실행 중 권한이 필요할 때 요청하는 것을 의미하며 요청에 대해 거부도 할 수 있으며, 승인/거부 상황에 맞게 처리하는 루틴이 고려해야 합니다.

 

요청을 하는 대상은 "위험 권한" 항목만 해당합니다. "일반 권한"은 자동으로 권한을 부여받습니다.

 

Android 6.0에 도입되었지만, 앱에서 targetSdkVersion을 23 이상으로 해주어야 실제 런타임 권한으로 동작합니다.

 

몇 달 전에 구글에서는 신규 및 기존 앱 업데이트 시 targetSdkVersion을 26이상으로 변경해야 등록되도록 정책을 변경하였습니다.
(관련 링크:
https://support.google.com/googleplay/android-developer/answer/113469?hl=ko#targetsdk)

 

그러므로, 이제부터는 구글플레이에 등록해야 하는 신규 앱은 처음부터 런타임 권한이 지원하도록 만들어야 하고, 기존 등록된 앱이 아직 targetSdkVersion을 23 이상으로 올리지 않았다고 하면 향후 업데이트 대비해서 런타임 권한이 지원시 문제가 될 수 있는 부분이 있는지 미리 파악해두시는 것이 좋을 것 같습니다.

 

런타임 권한 지원하기

 

아래와 같은 순서도로 동작하는 런타임 권한을 지원하도록 하겠습니다.

꼭 이렇게 해야 하는 것은 아니고, 하나의 예를 보여드리기 위함입니다.

 

 

 

런타임 권한과 관련된 API

권한 부여 여부: checkSelfPermission()

권한 요청: requestPermissions()

권한 요청 콜백: onRequestPermissionsResult()

사용자에게 설명이 필요한 상황 확인: shouldShowRequestPermissionRationale()

 


 

◈런타임 권한 지원하기 실제 예

지금부터는 런타임 권한 지원을 어떻게 하면 되는지 예제를 보면서 설명드리겠습니다. 아래에 있는 예제를 사용할 것입니다.

2018/10/13 - 손전등 앱 만들기 #2 - Camera2 API 이용방법 (Flashlight app using Camera2 API)

 

#1 targetSdkVersion 변경에 따른 이상 동작 확인

위에 언급한 예제는 아래와 같이 targetSdkVersion이 21로 런타임 권한 지원이 되지 않습니다.

android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.help.permissionflashlight3"
minSdkVersion 21
targetSdkVersion 21

먼저 런타임 관련한 코드 수정 없이 targetSdkVersion을 23으로 바꾸면 어떻게 되는지 확인해보겠습니다.

 

앱에서 카메라 장치에 접근(open)하는 순간 Exception이 발생해서 강제종료(Force close)됩니다. 

 

이는 targetSdkVersion이 23으로 되면서 "위험 권한"에 해당하는 카메라의 경우 자동으로 권한을 부여받지 못하고, 런타임 권한 요청에 의한 권한을 부여받아야 하기에 최초의 경우 권한이 없는 상태입니다. 그래서, CameraService 연결하려고 하면 권한 검사를 통과하지 못해 ERROR_PERMISSION_DENIED 상태로 연결 거부(Reject)되었음을 전달해주며 그 결과 CameraManger에서 SecurityException을 throw해서 Exception이 발생하게 됩니다.

[CameraService.cpp]

 

// If it's not calling from cameraserver, check the permission.
if (callingPid != getpid() &&
!checkPermission(String16("android.permission.CAMERA"), clientPid, clientUid)) {
ALOGE("Permission Denial: can't use the camera pid=%d, uid=%d", clientPid, clientUid);
return STATUS_ERROR_FMT(ERROR_PERMISSION_DENIED,
"Caller \"%s\" (PID %d, UID %d) cannot open camera \"%s\" without camera permission",
clientName8.string(), clientUid, clientPid, cameraId.string());
}
[CameraManager.java]

 

public static void
throwAsPublicException(Throwable t) throws CameraAccessException {
if (t instanceof ServiceSpecificException) {
ServiceSpecificException e = (ServiceSpecificException) t;
int reason = CameraAccessException.CAMERA_ERROR;
switch(e.errorCode) {
case ICameraService.ERROR_DISCONNECTED:
reason = CameraAccessException.CAMERA_DISCONNECTED;
break;
case ICameraService.ERROR_DISABLED:
reason = CameraAccessException.CAMERA_DISABLED;
break;
case ICameraService.ERROR_CAMERA_IN_USE:
reason = CameraAccessException.CAMERA_IN_USE;
break;
case ICameraService.ERROR_MAX_CAMERAS_IN_USE:
reason = CameraAccessException.MAX_CAMERAS_IN_USE;
break;
case ICameraService.ERROR_DEPRECATED_HAL:
reason = CameraAccessException.CAMERA_DEPRECATED_HAL;
break;
case ICameraService.ERROR_ILLEGAL_ARGUMENT:
case ICameraService.ERROR_ALREADY_EXISTS:
throw new IllegalArgumentException(e.getMessage(), e);
case ICameraService.ERROR_PERMISSION_DENIED:
throw new SecurityException(e.getMessage(), e);
case ICameraService.ERROR_TIMED_OUT:
case ICameraService.ERROR_INVALID_OPERATION:
default:
reason = CameraAccessException.CAMERA_ERROR;
}
throw new CameraAccessException(reason, e.getMessage(), e);
} else if (t instanceof DeadObjectException) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service has died unexpectedly",
t);
} else if (t instanceof RemoteException) {
throw new UnsupportedOperationException("An unknown RemoteException was thrown" +
" which should never happen.", t);
} else if (t instanceof RuntimeException) {
RuntimeException e = (RuntimeException) t;
throw e;
}
}

SecurityException에 의한 Fatal exception 대응 코드

아래와 같이 try ~ catch 사용으로 예외처리

try {
mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
...
}, null);
} catch (SecurityException e) {
e.printStackTrace();
}

#2 SecurityException의 root cause인 Runtime Permission 적용

SecurityException가 발생한 이유는 targetSdkVersion이 23으로 변경되었지만, RuntimePermission이 적용되어있지 않아서 Permission이 허용되지 않는 상태에서 문제가 된 API를 호출해서 발생하였습니다.

 

위의 순서도에 맞게 추가 작업한 부분입니다. (빨간색 코드)

 

최초 앱 실행 시 checkSelfPermission()을 호출해서 권한 부여 여부를 확인합니다.

권한을 이미 부여받은 상태이면, 원래의 액티비티(R.layout.activity_main)을 호출하고, 그렇지 않으면 권한 요청을 위한 새로 만든 액티비티(R.layout.activity_permission)을 호출하도록 하였습니다.

 

 

위의 권한 요청 액티비티(R.layout.activity_permission)에서 "요청하기" 버튼을 누르면 requestPermissions()를 호출해서 권한 요청을 하면 아래와 같은 "권한 요청 시스템 팝업"이 발생합니다.

 

 

위의 "권한 요청 시스템 팝업"에서 선택(거부/허용)한 결과는 onRequestPermissionsResult()로 콜백됩니다.

아래는 이와 관련된 기존 예제에서 추가된 코드입니다.

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_CAMERA) {
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
recreate();
} else if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(MainActivity.this, "잠시 후에 다시 권한 요청을 합니다.\n플래시를 사용하기 위해서는" +
" [카메라]권한이 필요합니다.", Toast.LENGTH_LONG).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
}, 3500);
} else {
Toast.makeText(MainActivity.this, "잠시 후에 앱 정보로 이동합니다. 권한 항목에서" +
" [카메라]를 허용해주세요.", Toast.LENGTH_LONG).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
}
}, 3500);
}
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

콜백에서 권한이 승인(PERMISSION_GRANTED)으로 넘어오면 recreate()를 호출해서 액티비티를 종료시키고 다시 액티비티(=MainActivity)를 실행하도록 하면 checkSelfPermission()에선 권한 부여받은 것으로 처리되어서 원래의 액티비티(R.layout.activity_main)를 호출해서 targetSdkVersion 23 이전처럼 앱이 정상동작하게 됩니다.

 

콜백에서 권한이 거부(PERMISSION_DENIED)로 넘어온 이후 두 번째 권한 요청할 때부터는 "권한 요청 시스템 팝업"에 "다시 묻지 않음" 체크 버튼이 추가되어서 나옵니다. 그래서, 위의 경우에 아래 왼쪽처럼 토스트 메시지가 나오고, 토스트가 사라질 때쯤에 오른쪽과 같이 "권한 요청 시스템 팝업"이 발생합니다.

 

 

위의 "권한 요청 시스템 팝업"에서 거부를 하면 다시 콜백되어서 토스트 메시지와 "권한 요청 시스템 팝업"이 다시 나오게 됩니다. 거부한 경우에 계속 반복적으로 요청하는 루틴으로 되어 있습니다. 이건 순서도에 있는 것처럼 이렇게 동작하도록 시나리오를 만든 것입니다. 이 부분의 처리는 각자 생각한 시나리오에 맞게 적절히 변경해주시면 됩니다.

 

만약 "다시 묻지 않음"을 체크한 경우에는 아래 왼쪽처럼 "허용"은 비활성화되고, "거부"만 선택할 수 있게 됩니다.

이때 "거부"를 선택하게 되면, 콜백에서 아래 가운데처럼 토스트 메시지가 나오도록 처리하고, 토스트가 사라질 때쯤 오른쪽과 같이 "앱 정보" 화면으로 넘어가도록 처리되어 있습니다.

 

참고: 2018/11/02 - 애플리케이션 정보 (Application info.) 보기로 이동

 

  

 

아래 왼쪽과 같이 "앱 정보"의 권한 항목에서 "카메라"를 허용해주고 앱으로 돌아가서 "요청하기" 버튼을 누르면 권한이 허용된 상태이기 때문에 원래의 액티비티(R.layout.activity_main)로 넘어갑니다.

 

"앱 정보"의 "카메라"를 허용하지 않는 상태로 앱으로 돌아가서 "요청하기" 버튼을 누르면 "다시 묻지 않음"이 체크된 상태라서 "권한 요청" 시 "권한 요청 시스템 팝업"이 나오지 않고 곧바로 "앱 정보"를 호출(아래 오른쪽 참고)하게 됩니다.

 

 

 

앱 동작 영상

 

프로젝트 화일

PermissionFlashlight3_support_runtime_permission.zip

여기까지입니다. 읽어주셔서 감사합니다.

 

 

 

 

+ Recent posts