일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 슬기로운 온라인 게임
- 어떻게 나답게 살 것인가
- 끝말잇기
- 한단어의힘
- 베드트레이
- T자형인재
- 1일1커밋
- 한달독서
- 소프시스
- 한달브런치북만들기
- 한달어스
- 소프시스 밤부 좌식 엑슬 테이블
- 테트리스
- 프래그먼트
- 안드로이드
- 북한살둘레길
- 아비투스
- 캐치마인드
- 좌식테이블
- 자취필수템
- 커스텀린트
- 목적 중심 리더십
- 목적중심리더십
- 브런치작가되기
- 지지않는다는말
- 베드테이블
- 재택근무
- 함수형 프로그래밍
- 면접
- 리얼하다
- Today
- Total
정상에서 IT를 외치다
카메라 예제와 함께 보는 Scoped Storage (안드로이드 Q 대응) 본문
안녕하세요. 블랙진입니다.
카메라 예제와 함께 보는 Scoped Storage 포스팅 중 드디어 마지막인 안드로이드 Q 대응하기 입니다!
<카메라 예제와 함께 보는 Scoped Storage>
먼저 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 정리
devloper.android/scoped-storage
'안드로이드' 카테고리의 다른 글
[Dagger] Dagger step5 - Binds와 Multi binding (0) | 2020.02.11 |
---|---|
[Dagger] Dagger step4 - Module 초기화 (0) | 2020.02.11 |
카메라 예제와 함께 보는 Scoped Storage (이미지 가져오기) (0) | 2020.01.13 |
[Android] Customing BottomNavView (0) | 2019.11.27 |
[Android, BottomSheet] 안드로이드 버텀 시트 사용 (0) | 2019.11.24 |