[베다니 도서관] 2. 저장소 접근 권한 설정을 해보자



1. 저장소 접근 방식

개발한 앱은 기본적으로 앱에 저장되어 있는 csv 파일을 탐색해서 가져오는 기능이 있기 때문에 저장소에 접근을 해야 했다. 저장소에 접근하는 메소드는 여러가지가 있지만 일단 몇 개 보자면


  • Environment.getDataDirectory() : /data
  • Environment.getDownloadCacheDirectory() : /cache
  • Environment.getRootDirectory() : /system
  • Environment.getExternalStorageDirectory() : /storage/sdcard0


정도가 있겠다. 사실 이 부분에서 이해를 잘못해서 헤맸었는데, getExternalStorageDirectory()가 sdcard0를 접근하는 메소드인데, 왜 sdcard0는 내부 저장소인가다.
알고보니 sdcard0는 내부 저장소, 즉 내부에 있는 sdcard이고 sdcard1이 외부에서 꽂는 sdcard(즉 우리가 추가로 구매해서 꽂는 그 sd카드이다.)이다. 한마디로 sdcard0는 "물리적으로 내장"되어 있다는 것이다. 결국 sdcard0도 내부에 존재하긴 하지만 외부 sdcard와 같은 방식이므로 getExternalStorageDirectory()로 접근하는 것이다.

어쩐지 /data나 /cache나 /system으론 파일을 찾을 수 없더니 저런 구조였다. 이 부분이 이해가 안되어서 시간이 좀 걸렸다. 어찌됐든 간에 
'getExternalStorageDirectory()로 접근을 해야하는 것을 알았다면, 이제 접근하기만 하면 해결!' 

이 아니다! 다른 문제가 존재한다.



2. 저장소 접근 권한 설정

내부 저장소 (여기서 내부 저장소는 data, cache, system을 말한다.)를 접근하는 데에는 특별한 권한 허가가 필요하지 않지만, 외부 저장소 접근과 같이 위험 권한은 사용자에게 권한 허가를 받아야 접근할 수 있다. 허가를 받지 않고 접근하려고 하면 정지하거나, 작동이 되지 않을 것이다.
스마트폰 앱을 사용하다 보면 권한 허용 여부를 묻는 메세지를 자주 보게 되는데, 이것이 접근 권한 허가를 받는 과정이다.. 이런 권한 허가는 개발자가 필요하다고 생각하면 추가시킬 수 있는데, 보통 위험 권한에 대한 허가 여부만 요청한다.

위험권한은 아래와 같다.



그렇다면 이런 권한 허가 메세지는 어떻게 만들 수 있을까? 코드를 보면서 이해해보자.
본 코드는 MainActivity와 AndroidManifest.xml에 작성하였다.



1. AndroidManifest.xml에 어떤 그룹에 접근을 할건지 권한 등록을 해준다.

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

2. Activity request 변수를 설정해준다.

private final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE = 1001;

3. 권한 허가를 받았는지 체크를 해준다.

//ContextCompat은 아래에서 설명하기로 하고, Manifest에 권한 등록이 되어있는지 확인하는 변수
int permissioncheck = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
        //권한이 없을 때 : 
        if(permissioncheck != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "권한 승인이 필요합니다.", Toast.LENGTH_LONG).show();
            //사용자가 거부를 한 경우
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                Toast.makeText(this, "외부 저장소 접근을 위해 저장소 접근 권한이 필요합니다.", Toast.LENGTH_LONG).show();
            }
            //최초 요청
            else {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE);
                Toast.makeText(this, "외부 저장소 접근을 위해 저장소 접근 권한이 필요합니다.", Toast.LENGTH_LONG).show();
            }
        }
위에서부터 보면 PackageManager에서 PERMISSION_GRANTED, 즉 권한 허가가 AndroidManifest.xml과 다르다면 (권한 허가가 안 떨어졌다면) 2가지 경우를 체크해본다.
1) 사용자가 접근 허가를 거부한 경우
shouldShowRequestPermissionRationale는 아래와 같은 flowchart를 가지는 메서드이다.


2) 최초 요청인 경우
Manifest의 허가 등록과 내 허가 여부 변수(위에서 1001로 설정)와 비교해서 요청해주자.

4. 권한 체크 후 콜백 메서드

@Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults){
        switch (requestCode){
            case MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE: {
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    Toast.makeText(this, "권한이 허가되어 있습니다.", Toast.LENGTH_LONG).show();
                }
                else{
                    Toast.makeText(this, "아직 승인받지 않았습니다.", Toast.LENGTH_LONG).show();
                }
                return;
            }
        }
    }

String permission[]은 권한 요청을 저장한 배열이며 int[] grantResults는 권한 허가 여부에 대한 결과값들을 저장한 배열이다. requestCode == 1001일 때 grantResults 배열에 값이 있고, 그 값이 권한 허가를 나타내는 값인지 아닌지를 체크해주면 된다.

아래는 전체 코드이다.


소스코드

private final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE = 1001;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int permissioncheck = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
        if(permissioncheck != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "권한 승인이 필요합니다.", Toast.LENGTH_LONG).show();

            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                Toast.makeText(this, "외부 저장소 접근을 위해 저장소 접근 권한이 필요합니다.", Toast.LENGTH_LONG).show();
            }

            else {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE);
                Toast.makeText(this, "외부 저장소 접근을 위해 저장소 접근 권한이 필요합니다.", Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults){
        switch (requestCode){
            case MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STROAGE: {
                if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    Toast.makeText(this, "권한이 허가되어 있습니다.", Toast.LENGTH_LONG).show();
                }
                else{
                    Toast.makeText(this, "아직 승인받지 않았습니다.", Toast.LENGTH_LONG).show();
                }
                return;
            }
        }
    }
++
ContextCompat? ActivityCompat?

뒤에 Compat이 붙은 것들은 Support 라이브러리인데, 옛날 버젼의 안드로이드에서 최신 버젼에서 쓸 수 있는 것들을 지원해주는 역할을 한다.
그렇다면 ActivityCompat은 정말 오래된 버젼에선 쓸 수 없는 Activity의 기능 중 shouldShowRequestPermissionRationale이나 requestPermissions같은 기능을 쓸 수 있게 해주는 것이겠다. 그렇다면 Context는 뭔가?
Context는 현재 사용되고 있는 앱에 대한 전역 환경 정보를 가지고 있는 것이란다. 즉 Activity에 대한 정보들을 저장하고 있는 것인데, 그렇다는 말은 Activity의 생명주기에 영향을 받는다는 얘기일 것이다. 즉, onDestroy가 호출되면 사라지게 된다는 얘기가 된다. 혹은 Intent를 통해 다른 Activity로 이동하면 이동한 Activity에 대한 정보도 이 Context에 쌓이게 될 것이다.
checkselfPermission은 앱이 시작되고 종료되기까지 존재할테니 ContextCompat에서 불러올 수 있는게 아닐까 라고 추측해본다.

댓글