안드로이드

Storage Scope

코딩하는후운 2021. 5. 14. 17:29
반응형

Storage Scope란?

- 개별 앱 공간이 샌드박스 형식으로 격리되어 다른 앱의 파일에 직접 접근할 수 없게 하는 방식을 말합니다.

 

targetSdkVersiond을 29로 올리면 아래와 같이 Deprecated 됩니다.

@Deprecated
public static File getExternalStorageDirectory() 

 

공용 저장소에 접근할 때는 MediaStore 또는 Intent(ACTION_OPEN_DOCUMENT)를 사용해야 된다고 합니다.

공용 공간은 MediaStore을 통해서만 읽고 쓸 수 있습니다. 혹은 SAF를 사용해 파일을 가져올 수 있습니다.

 

 

저장소

Android Q이전

외부 저장소

: 외부 저장소 권한이 있으면 누구나 접근이 가능
- READ_EXTERNAL_STORAGE 
- WRITE_EXTERNAL_STORAGE 

 

Android Q이후

샌드박스 저장공간
: 개별 앱만 접근 가능

- 읽고 쓰기 위한 별도의 권한 필요 없음
- Context.getExternalFilesDir(...)
- 앱 삭제 시 함께 삭제됨
- 앨범 아트, 썸네일 등 다른 앱과 공유할 필요가 없는 미디어 파일은 샌드박스 공간을 활용

 

공용 저장공간

- MediaStore.Audio
- MediaStore.Video
- MediaStore.Images
- 별도 권한 없이 파일 생성 가능
- 외부 저장소 권한으로 다른 앱이 생성한 파일 접근 가능

 

다운로드 저장공간
- MediaStore.Downloads
- 별도 권한 없이 파일 생성 가능
- 시스템 파일 선택기를 통해서만 다른 앱이 생성한 파일 접근 가능

 



다른 앱이 생성한 미디어 파일에 대한 접근 권한법

-MediaStore 사용할 경우(10 버전에서 새로 생김)

READ_MEDIA_IMAGES 

READ_MEDIA_VIDEO

READ_MEDIA_AUDIO 

 

-MediaStore 혹은 MediaStore 외의 방법을 사용할 경우

READ_EXTERNAL_STORAGE

 

-더 이상 추가 엑세스 제공 X- (권한요청시 false로 줬음)

WRITE_EXTERNAL_STORAGE

WRITE_MEDIA_STORAGE

 

앱이 생성한 미디어 파일에 접근하는 경우는 권한이 필요한데 

READ_MEDIA_IMAGES,  READ_MEDIA_VIDEO ,READ_MEDIA_AUDIO를 사용하시면 됩니다.

이것들은 안드로이드 10에서 새로 추가된 권한이고 각각 사진, 동영상, 음성을 읽는데 필요합니다.

 

기존의 READ_EXTERNAL_STORAGE 로도 가능한 일이었지만 구글은 앱에서 미디어 파일로 작업할 때는 광범위한 권한요청이아닌 필요한 권한만 요청하라고 권고하고있습니다

하지만 READ_EXTERNAL_SOTRAGE 를 현재 사용하고 계셔도 안드로이드 11버전에서 잘 동작하기 때문에 계속 사용하셔도 무관하다고 합니다.

 

 

안드로이드 11에서 MediaStore API 관련 method 들이 추가된게 있습니다. 

 

MediaStore API method 추가(언제 어떻게 써야할지 모르겠음)

: 스스로 만든 미디어에 대해서는 필요없고, 다른 앱에서 만든 미디어에 요청해야 한다고 하는데..

  1. createWriteRequest() : 앱에 지정된 미디어 파일 그룹에 관한 쓰기 액세스 권한을 부여하도록 사용자에게 요청
  2. createDeleteRequest() : 지정된 미디어 파일을 휴지통에 미리 넣지 않고 영구적으로 삭제하도록 사용자에게 요청
  3. createTrashRequest() : 지정된 미디어 파일을 기기의 휴지통에 넣도록 사용자에게 요청. 휴지통에 있는 항목은 정의한 기간이 지나면 영구적으로 삭제
  4. createFavoriteRequest() : 지정된 미디어 파일을 기기의 '즐겨찾기' 미디어로 표시 하도록 사용자에게 요청.

 

최상위 레벨 디렉토리명 더 이상 쓸 수 없다
: ROOT에 다운로드, SDCard 디렉토리 직접 지정 하는 경우(마이그레이션 이슈 발생)
-> OS변경을 감지해서 11로 업데이트 되었다면 기존 패스에서 새로운 패스로 미디어를 복사하는 작업이 필요하다(필요한 경우인듯)
-> 위에 번거로운 복사 작업을 방지하기 위해, 새로운 플레그가 추가되었다(preserveLegacyExternalStorage).

그렇게 하면 구 버전 API에서도 업그레이드 한 경우 마이그레이션을 바이 패스 할수있다.(?)
안드로이드 11에서 설치 or 재 설치한 경우에는 새로운 패스를 이용할수 있게 하고있다.

현재 Scoped Storage 외부에 저장 한 데이터를 마이그레이션하는 데 권장되는 방법은 무엇입니까?

  • preserveLegacyExternalStorage 플래그를 사용하면 Android 11을 대상으로하는 경우에도 앱이 업그레이드시 레거시 저장소 액세스를 유지할 수 있습니다.

 

MediaStore 을 통해 파일 불러오기

1. contentResolver.query()를 통해 파일의 uri를 받아올 수 있습니다.

val uri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI

 

val projection = arrayOf(

    MediaStore.Images.Media._ID,

    MediaStore.Images.Media.DISPLAY_NAME,

    MediaStore.Images.Media.DATE_TAKEN

)

 

val sortOrder = "$INDEX_DATE_ADDED DESC"

 

contentResolver.query(uri, projection, null, null, sortOrder)

 

2. cursor 를 통해 id를 불러와 withAppendedPath함수를 사용해 content:// 경로를 가져올 수 있습니다.

cursor.run {
    val idColumn = getColumnIndex(MediaStore.Images.Media._ID)

    val id = cursor.getLong(idColumn)


    val contentUri = Uri.withAppendedPath(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    id.toString()

)

Glide.with(this@StoreActivity)
.load(contentUri)
.into(imageView)

 

가져온 경로 > content://media/external/images/media/{id}

 

여기서 안드로이드 Q 미만에서는 '외부 저장소 중 앱 전용 공간'에 저장된 파일도 불러 왔으나 안드로이드 Q에서는 '앱 전용 공간'에 있는 파일은 못 가져오고 '공용 저장소'에 저장한 파일만 가져왔습니다.

이 부분을 통해 안드로이드 Q에서는 파일 접근에 대한 권한을 요구할 때 정말 필요한 권한인지를 다시한번 더 생각해 볼 필요를 느낄 수 있습니다.

 

MediaStore를 통해 파일 저장하기

1.ContentResolver에 데이터를 저장하기 위해서는 ContentValues클래스를 사용합니다.

    val values = ContentValues().apply{

    val fileName = "Days-${SystemClock.currentThreadTimeMillis()}.jpg"

    put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")

     //추가 경로를 설정
     //put(MediaStore.Audio.Media.RELATIVE_PATH, "DCIM/Days")


     //iS_PENDING 속성을 1로 해주는 것은 파일을 write 할 때 까지 다른 곳에서 사용 못하게 하는 것입니다.
     //파일을 모두 write 할때 이 속성을 0으로 update 해주어야 합니다.
     //현 포스팅은 카메라 예제로 외부에서 수정할 수 있어야 하므로 0으로 설정하거나 따로 처리 하지 않습니다.

     //1로 되어 있으면 카메라로 찍은 이미지가 저장되지 않습니다.

     //0으로 되어 있으면 카메라로 찍은 이미지가 저장 됩니다.

     
put(MediaStore.Images.Media.IS_PENDING, 0)

}

 

2. contentResolver를 사용해 MediaStore에서 사용할 수 있는 uri를 받아와 카메라로 넘겨줍니다.

여기서는 WRITE_EXTERNAL_STORAGE권한도 필요 없으며 ContentProvider로 감싸 주지 않아도 동작합니다.

> 이제는 파일 읽기 쓰기 권한을 받을 필요가 없는것 같습니다. 웬만한 기능들이 권한 없이 동작됩니다.

 

//권한이 없어도 진행 가능 합니다.
//경로 -> content://media/external/images/media/84
val contentUri: Uri? =

contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri)
startActivityForResult(intent, PICK_FROM_CAMERA)

 

3.onActivityResult에서 bitmap으로 이미지 보여주기

if (resultCode != Activity.RESULT_OK) {
    //만약 카메라를 사용해 사진을 찍지 않고 뒤로 가게 되면 생성한 uri를 제거해 주어야 합니다.

    //그렇게 하지 않으면 검은 화면의 빈 파일이 갤러리에 존재하게 됩니다.
    contentUriForAndroidQ?.let {
    contentUriForAndroidQ = null
    contentResolver.delete(it, null, null)

    return
    }
}


if (requestCode == PICK_FROM_ALBUM) {
//...
} else if (requestCode == PICK_FROM_CAMERA) {


    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        if(contentUriForAndroidQ != null) {

        //uri로 부터 이미지를 받아와 화면에 보여주면 됩니다.
        val source = ImageDecoder.createSource(contentResolver, contentUriForAndroidQ)
        setBitmap(ImageDecoder.decodeBitmap(source))
        }
    }
}

 

MediaStore을 통한 삭제

1.파일 삭제

가져온 uri를 contentResolver.delete()를 통해 파일을 지울 수 있습니다.

하지만 안드로이드 Q에서는 파일에 대한 수정 권한이 없기 때문에 삭제할 수 없습니다.

 

//매우 위험한 코드이다. 갤러리에 있는 모든 사진을 삭제해 버린다.
//하지만 안드로이드 Q 에서는 RecoverableSecurityException 에러가 발생합니다.
contentResolver.delete(contentUri, null, null)

 

2.권한을 가지고 삭제하기

안드로이드 Q 이상 버전에서는 아래와 같이 에러로 넘어온 intent를 사용해 사진을 삭제할 권한을 받아옵니다.

try {
    contentResolver.delete(contentUri, null, null)
} catch (e: RecoverableSecurityException) {
    // 권한이 없기 때문에 예외가 발생됩니다.
    // RemoteAction은 Exception과 함께 전달됩니다.
    // RemoteAction에서 IntentSender 객체를 가져올 수 있습니다.
    // startIntentSenderForResult()를 호출하여 팝업을 띄웁니다.
val intentSender = e.userAction.actionIntent.intentSender
    intentSender?.let {
        startIntentSenderForResult(
        intentSender,
        DELETE_PERMISSION_REQUEST,
        null,
        0,
        0,
        0,
        null
        )
    }
}

 

참조 :

https://developer.android.com/training/data-storage/shared/media?hl=ko
카메라 예제와 함께 보는 Scoped Storage (안드로이드 Q 대응)
https://black-jin0427.tistory.com/242
안드로이드 11 대응(Target SDK 30) - Scoped storage
https://developside.tistory.com/102
Scoped Storage(범위지정 저장소) 정리 (Legacy Storage와 차이점 정리)
https://youngest-programming.tistory.com/386

https://medium.com/androiddevelopers/android-11-storage-faq-78cefea52b7c

반응형

'안드로이드' 카테고리의 다른 글

SAF(Storage Access Framework)  (0) 2021.05.25
[Android] 이미지 로딩 라이브러리 - Coil  (0) 2021.05.17
ViewBinding  (0) 2021.05.13
Kotlin apply, with, let, also, run 사용?  (0) 2021.05.11
Android 멀티 모듈  (0) 2021.05.11