안드로이드는 6.0(마시멜로우, M)부터 펄미션을 위험 권한으로 구별하여 런타임 도중 사용자에게 허가 받도록 변화되었다.
그래서 보통 GPS(FINE_LOCATION), CAMERA, SENSOR(BODY_SENSORS)등등의 권한을 요청할때는 다음과 같이 코드를 작성하곤 했다.
override fun onCreate(~) {
~~~
if (ContextCompat.checkSelfPermission(this, PERMISSION) != PERMISSION_GRANTED))
ActivityCompat.requestPermissions(~~~~)
else
mainLogic()
}
override fun onRequestPermissionResult(~~~) {
~~~ //코드 체크
if (grantResults[0] == PERMISSION_GRANTED)
mainLogic()
}
그러면 이제 앱 실행시 펄미션 체크 후, 펄미션이 허가되어 있지 않으면 요청한 뒤, 요청 결과가 성공일경우 메인 로직을 실행하는 방식으로 구성하곤 한다.
하지만, 이 때문에 분기점이 3개나 생성된다.
1. 펄미션이 허가되어 있지 않은경우
1-1. 펄미션 요청시 요청을 허가하지 않았을때
1-2. 펄미션 요청시 요청을 허가한경우
2. 펄미션이 허가된 경우
이때, 1-2.와 2.는 메인로직으로 들어가기는 하지만, 동일한 함수를 두번씩이나 써야 했다. (체크부분(2), 펄미션 결과부분(1-2))
또, 펄미션 허용 결과를 처리할때도 onCreate에서 벗어나 PermissionResult 함수에서 처리를 하게 되는데, 이 부분 역시 약간 함수가 중간에 잘린듯한? 좀 깔끔하지 않은 방식으로 느껴졌다.
ActivityResultLauncher라는 객체를 생성해 펄미션을 요청할 수 있다.
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted)
mainLogic()
}
최근에는 위 RequestPermissionResult방식 대신 ActivityResultLauncher라는 객체를 이용해서 콜백 방식으로 결과를 처리할 수 있게 되었다. 위 방식대로 구성한 뒤, onCreate에서 launcher.launch(~)를 호출한다면 동일한 결과를 얻을 수 있었다.
하지만, 위 방식 또한 단점이 있었다. 콜백 방식으로 결과를 구성할 수 있어, 범용 애플리케이션에서 사용할 수 있는 클래스로 제작하고자 다음과 같이 구성했었다.
class PermissionRequest(private val permissions : Array<String>, private val onSuccess : (() -> Unit)?, private val onFailure : (() -> Unit)) {
fun request() {
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted)
onSuccess?.invoke()
else
onFailure?.invoke()
}
launcher.launch(permissions)
}
}
class PermissionRequestBuilder {
private var successCallback : (() -> Unit)? = null
private var failureCallback : (() -> Unit)? = null
fun onSuccess(successCallback : (() -> Unit) : PermissionRequestBuilder {
this.successCallback = successCallback
}
fun onFailure(failureCallback : (() -> Unit) : PermissionRequestBuilder {
this.failureCallback = failureCallback
}
fun execute() {
PermissionRequest(successCallback, failureCallback).request()
}
}
PermissionRequestBuilder()
.onSuccess { mainLogic() }
.onFailure { exit() }
.execute()
이런식으로 콜백을 이용한 깔끔한 요청방식을 꿈 꾸었으나...
ActivityResultLauncher는 onCreate함수에서만 초기화가 가능했다. 물론 여기에 대응하게 onCreate에서만 request를 호출하고 하는 방법도 있지만, 이렇게 될경우 테스트 용이성이 사라지게 된다. (안드로이드 Lifecycle에 클래스가 종속되버림)
야심차게 생각한 아이디어를 토대로 프로젝트에 적용해서 CODING GOSU가 되려 했으나 안되서 슬펐다.
하지만, 이 방법을 생각해본 사람들이 있지 않을까? 누군가는 해결했지 않았을까 라는 기대를 안고 구글을 찾아봤다.
https://github.com/guolindev/PermissionX
https://github.com/ParkSangGwon/TedPermission
두개나 있었다!
PermissionX.init(activity)
.permissions(Manifest.permission.READ_CONTACTS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE)
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this, "These permissions are denied: $deniedList", Toast.LENGTH_LONG).show()
}
}
PermissionListener permissionlistener = new PermissionListener() {
@Override
public void onPermissionGranted() {
Toast.makeText(MainActivity.this, "Permission Granted", Toast.LENGTH_SHORT).show();
}
@Override
public void onPermissionDenied(List<String> deniedPermissions) {
Toast.makeText(MainActivity.this, "Permission Denied\n" + deniedPermissions.toString(), Toast.LENGTH_SHORT).show();
}
};
TedPermission.create()
.setPermissionListener(permissionlistener)
.setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
.setPermissions(Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
.check();
둘다 구현방식이 비슷했지만, 최종적으로는 콜백 또는 리스너를 활용해서 동적으로 요청 결과를 처리할 수 있었다.
분명 내가 생각한 코드랑 유사한데, 이분들은 어떻게 구현했을까라는 생각이 들었다.
그래서 코드를 분석해보기로 했다.
protected void checkPermissions() {
~~~
TedPermissionActivity.startActivity(context, intent, listener);
TedPermissionUtil.setFirstRequest(permissions);
}
펄미션을 다룰때 특정 액티비티를 실행했다..?
fun requestNow(permissions: Set<String>, chainTask: ChainTask) {
invisibleFragment.requestNow(this, permissions, chainTask)
}
보이지 않는 프래그먼트를 처리했다..?
위 라이브러리들의 공통점은 가상 액티비티 또는 프래그먼트를 생성한다. 이후, 콜백을 해당 액티비티에 전달해서 로직을 처리한다음, 처리가 끝난 후 메인으로 돌아오는 방식을 취하고 있었다.
왜 이러한 방식을 사용하는지 생각해봤더니 어쨌든 onPermissionResult나 ActivityResultLauncher에서 펄미션 결과를 처리해야 하는데, 이는 onCreate나 override된 메소드를 가지고 있는 '액티비티' 또는 '프래그먼트'에서 처리할 수 밖에 없었다.
따라서, 가상 액티비티 또는 프래그먼트를 생성해서 처리하는 방법을 사용한것 같다.
프래그먼트나 액티비티를 사용하더라도, 해당 소스로 콜백 객체를 어떻게 전달했는지 살펴봤는데, java의 static 혹은 companion 객체를 이용해서 전역적인 callback 저장소 (List, Array)등을 두고, 거기서 처리하는 방식으로 이루어진것 같았다.
companion object {
private val callbackStore : List<Listener> = mutableListOf()
fun request(permissions : Array<String>) {
InvisibleActivity.callbackStore.add(listener)
val intent = Intent(~::class.java, InvisibleActivty::class.java).apply {
putExtra("perms", permissions)
}
startActivity~()
}
}
override fun onCreate() {
val perms = intent.getExtras("perms")
~~~
requestPermissions(perms)
}
override fun onPermissionResult() {
callbackStore.forEach { callback ->
callback(grantedPermission)
}
}
이런 느낌으로 처리한것 같다.
역시 코딩 고수들이 많다. 내가 생각하지 못했던 방식으로 이러한 문제점을 해결한게 신기했다.
하지만, 위 방식은 특정 액티비티를 호출하는 방식인데, 화면 회전이나 여러가지 상황에서 onCreate가 다시 호출되는 등 불안정할 수 밖에 없다. 이 때문에 코드내에서도 화면 회전등을 막아놓기도 하였고.
실제 프로젝트에 적용할때는 이러한 문제점에 대해 한번 살펴보고, 안전하다 생각될때 적용해보고자 한다.