박상권의 삽질블로그

[안드로이드/Android]6.0 마시멜로우 권한체크하고 최적화하기 본문

IT/Android-TIP (한글)

[안드로이드/Android]6.0 마시멜로우 권한체크하고 최적화하기

박상권 2016. 2. 19. 10:12

제가 운영하고 있는 유튜브 채널 '개발자 테드박'에도 많은 관심 부탁드려요.
스타트업/개발자/IT 관련된 여러 영상을 올리고 있습니다.
영상보러가기



블로그를 Medium으로 옮겨서 운영하고 있습니다.
앞으로 새로운 글은 모두 미디엄 블로그를 통해서 올릴 예정입니다.
미디엄에서 다양하고 유익한 포스팅을 살펴보세요
미디엄 블로그 보기



지난 2015년 5월에 열린 Google I/O에서 안드로이드 6.0 마시멜로우가 공개되었습니다.

여러가지 개선사항들중 우리 개발자들에게 큰 영향력을 끼칠수 있는 권한획득 방식이 변경되었습니다.

물론, 2016년 2월을 기준으로 현재 마시멜로우이상의 기기는 1.2%밖에 되지 않지만 앞으로 그 비율은 점점 늘어날 것입니다.(안드로이드 OS 점유율)

(2018년 1월기준으로는 55.6%까지 올라왔습니다)


현재 운영중인 앱에서 안드로이드 6.0 M(MarshMellow)버전과 관련된 오류가 없을수도 있지만 해당버전에 대해서 대응해 놓지 않을경우 점점 수많은 오류를 맞이하게 될것입니다.

이번 포스팅에서는 Permission획득 방식과 어떻게하면 좀더 효율적이게 사용할수 있을지에 대해 알아 보겠습니다.






Runtime Permission


이전까지 우리는 안드로이드의 권한을 관리하는것에 대해서 큰 어려움을 느끼지 않았습니다.

사용하는 권한들을 AndroidManifest.xml에 선언해두고 사용자가 앱을 설치하는 시점에 한번만 동의를 받으면 그 이후에는 문제없이 해당 권한들을 사용할 수 있었기 때문입니다.




사용자가 해당 권한을 이 앱에서 사용하는것에 대해서 꺼림직해 하거나 맘에 안든다면 앱을 사용하지 않는 방법밖에 없습니다.

그로인해 수많은 피싱앱이 생기기도 했고 일부 앱에서는 과도한 권한을 요구하는경우도 있었습니다.

(앱하나 다운받았더니 '접근권한' 44개 요구…'유리폰' 방지법 발의)









하지만 이제부터 안드로이드 6.0 마시멜로우버전 이상에서는 설치할때 권한허용 여부를 묻지 않습니다.

앱에서 해당 권한이 필요할때마다 사용자로부터 권한을 허가받도록 변경되었습니다.

또한, 사용자가 권한을 허가했더라도 사용자는 설정화면(설정 > 애플리케이션 > 앱이름 > 권한)을 통해 언제든지 권한을 허용/거부 할 수 있습니다.

그래서 우리는 해당 권한이 실행될때마다 권한을 사용할 수 있는지 확인해야하고, 권한을 사용할 수 없는경우에는 사용자로부터 권한을 허가받는 기능을 추가해주어야 합니다.


            

                    

       







아직 마시멜로우에 대응할 마음의 준비가 안되셨나요?



지금 마시멜로우 폰에서 플레이스토어에 있는 내 앱을 설치하면 오류가 발생할까요?

그렇지 않습니다.

안드로이드에서 targetSdkVersion가 23버전보다 아래라면 앱이 설치되면 모든 권한이 허용되어있는 상태에서 시작합니다.

아직 마음의 준비가 안되셨다면 targetSdkVersion을 22로 두고 개발하세요.

단, 이미 targetSdkVersion을 23으로 올리셨다면 22로 내릴수 없습니다.

저는 이사실도 모르고 23으로 바로 올려버려서 마시멜로대응을 최대한 빨리 하는 상황을 맞이하였습니다..


하지만 위에서 언급한것처럼 사용자가 직접 설정페이지에서 해당 권한을 거부할 수도 있습니다.

사용자가 수동으로 설정페이지에서 권한을 없애버리면 내 앱은 오류가 발생할까요?

앱이 오류가 발생해서 죽지는 않습니다. 해당 권한을 사용하는 기능을 사용하지 못할뿐입니다.

현재 배포되어있는 앱은 오류를 방지하기위한 최소한의 장치로 생각하고 우리는 최대한 빨리 마시멜로우 권한획득에 관한 대응을 해놓아야 합니다.







모든 권한에 대해서 체크해야 할까요?


그렇지 않습니다.

당연히 AndroidManifest.xml에서 선언한 모든 권한에 대해서 허가를 받아올 필요는 없습니다.

구글이 정의한 Normal Permission과 Dangerous Permission중 Dangerous Permission에 대해서만 권한을 체크해주면 됩니다.

Normal and Dangerous Permissions


아래 표는 꼭 Permission을 체크하고 허가를 받아야 하는 Dangerous permissions와 permissions groups입니다.

이외의 Permission들은 체크하지 않으셔도 됩니다.


Permission Group

Permissions
CALENDAR
CAMERA
CONTACTS
LOCATION
MICROPHONE
PHONE
SENSORS
SMS
STORAGE








권한 체크하고 요청하기


우리는 이제 권한을 체크하고 사용자에게 요청한뒤 다시 해당 결과값을 받아와야합니다.

이러한 과정에서 우리는 아래 함수들을 사용해야 합니다.





-권한을 가지고있는지 체크하기


ContextCompat.checkSelfPermission()


int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR);

if(permissionCheck== PackageManager.PERMISSION_DENIED){

// 권한 없음
}else{
// 권한 있음
}







- 해당 권한이 필요한 이유 설명해야하는 경우인지 알아오기


shouldShowRequestPermissionRationale()

FragmentCompat와 ActivityCompat 클래스에서 이 함수를 사용해서 체크할 수 있습니다.

해당 함수가 true를 리턴하는경우 우리는 왜 해당 권한이 필요한지를 설명해주고나서 권한 허가를 요청해야 합니다.

만약 권한허가요청 다이어로그에서 사용자가 [다시 묻지 않기]를 체크했다면 이 함수는 항상 false를 리턴합니다.

처음 권한을 요청하는경우에 이 함수는 항상 false를 요청합니다.

즉 사용자가 [다시 묻지 않기]를 체크하지 않고, 1번이상 권한요청에 대해 거부한 경우에만 true를 리턴해주고 있습니다.

왜 처음 권한을 요청할때 true를 리턴하지 않는지 이해가 가지 않습니다.

아래에서 설명하겠지만 저는 이 함수를 활용하지 않습니다.







- 권한 허가 요청하기


requestPermissions()

FragmentCompat와 ActivityCompat 클래스에서 이 함수를 사용해서 사용자에게 권한허가를 요청하는 다이어로그를 띄울수 있습니다.


(READ_CONTACTS 권한허가를 요청할때의 예제)


// Activity에서 실행하는경우
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED) {

// 이 권한을 필요한 이유를 설명해야하는가?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.READ_CONTACTS)) {

// 다이어로그같은것을 띄워서 사용자에게 해당 권한이 필요한 이유에 대해 설명합니다

// 해당 설명이 끝난뒤 requestPermissions()함수를 호출하여 권한허가를 요청해야 합니다

} else {

ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);

// 필요한 권한과 요청 코드를 넣어서 권한허가요청에 대한 결과를 받아야 합니다

}
}







권한허가 요청후 결과 가져오기


onRequestPermissionsResult()

이 함수는 Activity의 onActivityResult()와 비슷한 개념입니다.

위의 예제에서 MY_PERMISSIONS_REQUEST_READ_CONTACTS로 보낸 요청코드에 대해서 결과값을 가져오고 그에대한 처리를 해주어야 합니다.


@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS:

if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
         // 권한 허가
// 해당 권한을 사용해서 작업을 진행할 수 있습니다
         } else {
         // 권한 거부
// 사용자가 해당권한을 거부했을때 해주어야 할 동작을 수행합니다
}
return;
}
}









최적화하기



- Intent를 활용하자


사용자에게 하는 권한허가요청을 줄이는것이 사용자와 개발자 모두의 정신건강에 좋습니다.

그래서 꼭 해당 권한을 사용하지 않고 intent로 대체해서 사용할 수 있다면 그렇게 해주는것이 좋습니다.

만약 카메라로 사진을 찍고 이미지를 가져와야하는경우 카메라뷰를 커스텀해서 사용하는것이 아니라면 CAMERA권한을 허가요청할 필요 없이 ACTION_IMAGE_CAPTURE를 이용해 intent를 날리면 좋습니다.

Intent중 권한이 필요없는 것들을 살펴보시고 권한허가를 요청하지 않고 구현해보시기 바랍니다.

안드로이드에서 제공하는 Intent목록



+2016.06.30

그런데 이것은 100% 맞는 상황이 아니었습니다.

만약 Manifest에 아래와 같이 카메라권한을 추가한 경우라면 ACTION_IMAGE_CAPTURE를 사용하더라도 카메라권한을 허가받아야 합니다...

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

다른 부분에서 사용되는 카메라권한인데도 선언되어있다는 이유만으로 허가를 받아야 하다니 이상한 부분입니다...

Manifest에 카메라권한이 선언되어있지 않다면 위에 쓰여진대로 권한허가 없이 ACTION_IMAGE_CAPTURE 인텐트만 날려도 사용할 수 있습니다.





- 안드로이드 4.4 킷캣(API 19)이상이라면 READ_EXTERNAL_STORAGE 권한이 필요 없습니다


4.4 이전버전에서는 파일을 읽어오려면 READ_EXTERNAL_STORAGE 권한이 꼭 필요 했었습니다.

권한이 없다면 getExternalStoragePublicDirectory()를 사용할 수 없었습니다.

하지만 4.4 킷캣이상부터는 권한이 필요 없이 getExternalFilesDir(String), getExternalCacheDir()를 통해 파일을 가져올 수 있습니다.

Android 4.4 APIs Important Behavior Changes






- 필요한 시점에만 권한을 요청하자


네 맞습니다. 사실 귀찮습니다.

개발자 입장에서 생각해보면 권한을 사용할때마다 권한을 체크하고, 사용자로부터 허가를 요청하고 또 그에대한 처리를 해주는 과정이 너무 복잡하고 귀찮습니다.

그래서 일부 앱에서는 앱을 시작할때부터 모든 권한을 요구하는 경우도 있습니다.

가장 많은 사용자가 있을것 같은데 [올레 고객센터]앱은 이렇게 만들어져 있습니다.




앱을 시작하자마자 사용자로부터 6개의 권한허용을 요구합니다.

개발자 입장에서는 앱 처음에만 권한을 검사해서 처리하면 편하지만, 사용자입장에서는 굉장이 불쾌하고 불편하게 만듭니다.

왜 각각 6개의 권한이 여기서 쓰이는지에 대한 설명도 없을뿐더러, 아직 사용하지도 않았고 사용하지 않을지도 모르는 권한들을 애초에 요청한다는것 자체가 좋지 않은 방법 이기 때문입니다.

그렇기 때문에 우리는 해당권한을 사용하게 되는 시점에 해당 권한 허가여부를 체크하고 사용자에게 권한허가를 요청해야합니다.







- 왜 이 권한이 필요한지 알려주자


위에서 말씀드린것처럼 shouldShowRequestPermissionRationale()를 이용하면 이유를 알려줘야 하는 시점을 알 수 있습니다.

하지만 맨처음 권한을 요청하는경우에 이 함수는 항상 false를 리턴하기때문에 사용자에게 권한이 필요한 이유를 설명하는데 이 함수는 필요 없다고 판단했습니다.

그래서 사용자가 해당 권한을 거부했을때 왜 이 권한이 필요한지 알려주고, 

이미 거부를 했더라도 사용자가 권한을 허가해주길 원하는경우 직접 [설정]에 들어가서 권한을 추가해줄 수 있도록 다이얼로그를 만들어주었습니다.




- 만약 권한요청을 처음하는데도 권한 요청하는 시스템팝업이 안뜬다면?


의심 1

: 권한이 Manifest에 잘 정의되어있고 올바른 권한 이름으로 요청하는지 확인하세요


의심 2

: 라이브러리에서 해당 권한을 Manifest에 android:maxSdkVersion로 넣어두는경우, 해당 버전보다 디바이스 버전이 높다면 시스템팝업이 뜨지 않습니다.

<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="19" />

: 이럴경우 해당 권한의 설정을 내 app의 Manifest설정으로 재정의 해줘야 합니다.

<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="replace" />










그래도 여전히 귀찮습니다


위의 최적화하기에서 말씀드린것처럼 우리는 권한을 사용하는 시점에 권한을 체크하고 사용자에게 권한허가를 요청해야 합니다.

네 알고 있죠 암요...알고 있습니다.

하지만 우리는 너무 귀찮습니다.

권한체크 이벤트가 발생할때마다

- 권한을 체크해야 하고(checkSelfPermission)

- 권한을 요청해야 하고(requestPermissions)

- 권한요청에 대한 결과를 받아서 판단해야합니다(onRequestPermissionsResult)

- 또한 위의 최적화하기에서 사용자가 권한을 허용하지 않았더라도 다이얼로그에서 설정에서 직접가서 권한을 허용할수 있게 안내할수 있는데 그러한 경우에 사용자가 설정화면에서 다시 앱으로 돌아온경우(onActivityResult)를 확인해서 권한이 허용되었는지 아닌지를 판단해야만 합니다.


물론 잘하시는 개발자분들은 공통으로 사용하는 부분들을 BaseActivity나 PermissionHelper라는 개념의 클래스를 만들어서 최대한 코딩량을 줄이시려고 노력하고 계실겁니다.

하지만 그래도 여전히 뭔가 불편함을 느끼시고 계실겁니다.

어떠한 방법을 쓰더라도 결국엔 BaseActivity와 BaseFragment에 각각 onRequestPermissionsResult()를 만들어주어야 할 것이고, 그 외에도 어쩔수 없이 반복해야하는 작업들이 있을겁니다.

저도 정말 수많은 고민을 하고 이 귀찮음 때문에 잠을 이루지 못했습니다.

사실 잠은 잘 잤습니다.


다른 개발자분들의 최적화에 대한 고민 담긴 블로그입니다.

리멤버의 안드로이드 6.0 M버전 대응기









그래서 TedPermission라이브러리를 만들었습니다


TedPermission: Easy check permission library for Android Marshmallow



사용방법은 간단합니다.

권한요청에 대한 결과를 받아오는 리스너를 만들고, 권한을 체크하기만 하면 됩니다.

TedPermission이 알아서 다 해줍니다.

[IT/Android-TIP (한글)] - [안드로이드/Android]유용한 라이브러리 - TedPermission(마시멜로우 권한체크)


PermissionListener permissionlistener = new PermissionListener() {
@Override
public void onPermissionGranted() {
Toast.makeText(Activity1.this, "권한 허가", Toast.LENGTH_SHORT).show();
}

@Override
public void onPermissionDenied(ArrayList<String> deniedPermissions) {
Toast.makeText(Activity1.this, "권한 거부\n" + deniedPermissions.toString(), Toast.LENGTH_SHORT).show();
}


};

TedPermission.with(this)
.setPermissionListener(permissionlistener)
.setRationaleMessage("구글 로그인을 하기 위해서는 주소록 접근 권한이 필요해요")
.setDeniedMessage("왜 거부하셨어요...\n하지만 [설정] > [권한] 에서 권한을 허용할 수 있어요.")
.setPermissions(Manifest.permission.READ_CONTACTS)
.check();











경우의수


아래내용은 TedPermission에서 사용하는 권한체크 Workflow입니다.








1. Permission 체크 > 권한이 허용되어 있는 경우

: onPermissionGranted()를 호출합니다.


2. Permission 체크 > 권한이 허용되어 있지 않은 경우

: 권한허가를 요청하는 다이얼로그가 실행됩니다.




3. 권한허가 다이얼로그 > 허용

: onPermissionGranted()를 호출합니다.



4. 권한허가 다이얼로그 > 거부

: 권한이 거부되는경우 위에서 설명한대로 해당 권한이 필요한 이유와 함께 사용자가 설정에서 수동으로 권한을 추가해줄수 있도록 [설정]버튼을 함께 보여줍니다.




5. 권한거부 및 권한이유설명 다이얼로그 > 닫기

: onPermissionDenied()를 호출합니다.



6. 권한거부 및 권한이유설명 다이얼로그 > 설정

: 앱의 설정페이지로 이동합니다. 이때 startActivityForResult()로 실행해주기 때문에 다시 앱으로 돌아올때는 onActivityResult()에서 받을 수 있습니다.




7. 설정 액티비티 > onActivityResult()

: 해당권한이 허용되어있는지를 체크합니다.


8. 해당권한 허용여부 체크 > 허용되어있음

: onPermissionGranted()를 호출합니다.


9. 해당권한 허용여부 체크 > 허용되어있지 않음

: onPermissionDenied()를 호출합니다.





TedPermission 라이브러리의 상세 구현방법에 대해는 다음번 포스팅에서 다뤄보도록 하겠습니다.

물론 GitHub에서 데모 프로젝트를 다운받으신뒤에 구조를 파악하셔도 좋습니다.

사실 여기에 다 올리려고 했으나 체력적인 한계로 인해 다음포스팅으로 미룹니다.







지금까지 안드로이드 6.0 마시멜로우버전에서 변경된 권한획득 방법에 대해 포스팅 해보았습니다.

어때요 참 쉽죠?




권한체크 방식으로 인해 소스코드 대부분을 뒤집어 엎어야 하는 상황이 발생할수도 있지만 구글형이 까라면 까야죠..

사실 마시멜로우에서 변경된 Doze모드에 대한 대응도 아직 하지 않았죠.

아직도 갈길이 머네요..

앞으로의 버전에서는 또 어떻게 바뀔지 기대(걱정)됩니다.



감사합니다.





안드로이드 개발자끼리 소통하기위한 오픈채팅방을 만들었습니다.

안드로이드 관련 Q&A및 팁을 공유하는 곳입니다.

관심있으신분들은 참여해보세요.

https://open.kakao.com/o/g8rSGB

Comments