정상에서 IT를 외치다

카메라 예제와 함께 보는 Scoped Storage (안드로이드 Q 대응) 본문

안드로이드

카메라 예제와 함께 보는 Scoped Storage (안드로이드 Q 대응)

Black-Jin 2020. 1. 13. 17:43
반응형

안녕하세요. 블랙진입니다.


카메라 예제와 함께 보는 Scoped Storage 포스팅 중 드디어 마지막인 안드로이드 Q 대응하기 입니다!


<카메라 예제와 함께 보는 Scoped Storage>


권한 가져오기

저장소의 종류

이미지 가져오기

안드로이드 Q 대응



먼저 targetSdkVersion을 29로 올렸을 때 저장소 경로를 가져오는 코드의 변화부터 살펴보겠습니다. 앞선 저장소 예제에서 설명했듯이 아래는 공용 저장소를 가져오는 코드 입니다.

Timber.e("--외부 저장소--")
//경로 storage/emulated/0
Timber.d("getExternalStorageDirectory1 : ${Environment.getExternalStorageDirectory()}")
//경로 storage/emulated/0/Pictures
Timber.d(
"getExternalStorageDirectory2 : ${Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)}"
)


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

 * @see #getExternalStorageState()
* @see #isExternalStorageRemovable()
* @deprecated To improve user privacy, direct access to shared/external
* storage devices is deprecated. When an app targets
* {@link android.os.Build.VERSION_CODES#Q}, the path returned
* from this method is no longer directly accessible to apps.
* Apps can continue to access content stored on shared/external
* storage by migrating to alternatives such as
* {@link Context#getExternalFilesDir(String)},
* {@link MediaStore}, or {@link Intent#ACTION_OPEN_DOCUMENT}.
*/
@Deprecated
public static File getExternalStorageDirectory() {
throwIfUserRequired();
return sCurrentUser.getExternalDirs()[0];
}


이 경우 어떻게 해야 될지에 대해 구글은 주석에 상세히 적어 놓았습니다. 공용 저장소에 접근할 때는 MediaStore 또는 Intent(ACTION_OPEN_DOCUMENT)를 사용해야 된다고 바로 확인 할 수 있습니다.



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


- Scoped Storage에서 공용 공간은 "사진 및 동영상" , "음악" , "다운로드" 로 이뤄줘 있습니다.

- 권한이 없어도 공용 공간에 파일을 만들고 수정할 수 있습니다. 하지만 자신이 만든 파일만 접근할 수 있고 앱 제거시 삭제 됩니다.


위 두 내용을 코드로 살펴보겠습니다.




MediaStore 을 통해 파일 불러오기


1. READ_EXTERNAL_STORAGE 또는 WRITE_EXTERNAL_STORAGE 권한을 요청합니다.

-> 여기서 query를 할때 권한이 없어도 동작은 합니다. 하지만 권한이 없는 경우에는 내가 생성한 파일만 확인할 수 있습니다.


2. 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)


3. 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}


쿼리를 통해 가져온 uri를 사용해 imageView에 이미지를 보여주는 예제 코드입니다. 위 코드에서는 Glide를 사용해 이미지를 보여주고 있습니다. 여기서 안드로이드 Q 미만에서는 '외부 저장소 중 앱 전용 공간'에 저장된 파일도 불러 왔으나 안드로이드 Q에서는 '앱 전용 공간'에 있는 파일은 못 가져오고 '공용 저장소'에 저장한 파일만 가져왔습니다. 이 부분을 통해 안드로이드 Q에서는 파일 접근에 대한 권한을 요구할 때 정말 필요한 권한인지를 다시한번 더 생각해 볼 필요를 느낄 수 있습니다.




MediaStore 을 통해 파일 저장하기


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

val values = ContentValues().apply {

val fileName = "BlackJin-${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/BlackJin")


//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으로 이미지 보여주기

override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)

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를 contentReslover.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
)
}
}


예물레이터를 이용한 테스트 화면 입니다. 아래와 같이 삭제를 허용하겠냐는 메시지를 보여줍니다.





<참고자료>

안드로이드 Q Scoped Storage 이해하기
개발자를 위한 안드로이드 Q 정리


codechacha basic

devloper.android/scoped-storage

Preparing for Scoped Storage (YouTube)

Working with Scoped Storage

반응형
Comments