일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 (이미지 가져오기) 본문
안녕하세요. 블랙진입니다.
지난 저장소 관련 포스팅에 이어 기존 저장소 방식에서는 어떻게 이미지를 가져오고 사용하는지 살펴보겠습니다.
<카메라 예제와 함께 보는 Scoped Storage>
갤러리에서 사진 가져오기
갤러리에서 사진을 가져올 때는 아무 권한도 필요 없습니다. 그저 Intent.ACTION_PICK를 통해 Media에 접근할 수 있습니다.
val intent = Intent(
Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI
)
startActivityForResult(intent, PICK_FROM_ALBUM)
혹은 Intent.EXTRA_ALLOW_MULTIPLE 을 사용해 어떤 앨범앱을 사용할지 선택할 수 있습니다.
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_FROM_ALBUM)
갤러리에서 사진을 선택하면 onActivityResult로 이동합니다.
여기서 안드로이드 P를 기준으로 아래와 같은 코드를 통해 uri를 bitmap으로 변환할 수 있습니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
if (requestCode == PICK_FROM_ALBUM) {
intent?.data?.let { photoUri ->
/**
* 읽기 권한이 없어도 동작됩니다.
*/
val selectedImage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
/**
* 하드웨어 가속이 필요
* java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps
*/
val source = ImageDecoder.createSource(contentResolver, photoUri)
ImageDecoder.decodeBitmap(source)
} else {
contentResolver.openInputStream(photoUri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}
}
setBitmap(selectedImage)
return
}
}
}
혼돈주의!
참고로 이런 코드도 보았을 것입니다. cursor를 사용해 bitmap 파일을 가져올 경우에는 파일 읽기 권한이 필요합니다.
/**
* 컨텐트 URI : 데이터 프로파이더 안에 있는 데이터를 정의한다.
* 컨텐트 URI는 프로바이더에 해당하는 고유한 이름을 가지고 있는데, 이것을 권환이라 하고, 테이블을 나타내는 경로로 이루어져 있다.
*
* uri 경로 -> content://media/external/images/media/...
* 절대 경로 -> /storage/emulated/0/DCIM/Camera/...
*/
val cursor = contentResolver.query(
photoUri, // content://scheme 방식의 원하는 데이터를 가져오기 위한 정해진 주소
null,
null,
null,
null
)
if (cursor != null) {
//커서에 데이터가 들어 있는지 확인합니다.
if (cursor.count > 0) {
//커서를 처음 위치로 이동시킴
cursor.moveToFirst()
//컬럼 인덱스를 가져옴
val columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
//외부저장소 경로를 가져옴
val path = cursor.getString(columnIndex)
setBitmap(BitmapFactory.decodeFile(path))
}
cursor.close()
}
카메라에서 이미지 가져오기
다음으로 카메라에서 이미지를 가져오겠습니다.
private fun takePhoto() {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
tempFile = createImageFile()
tempFile?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val photoUri = FileProvider.getUriForFile(
this,
"${application.packageName}.provider",
it
)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
startActivityForResult(intent, PICK_FROM_CAMERA)
} else {
val photoUri = Uri.fromFile(it)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
startActivityForResult(intent, PICK_FROM_CAMERA)
}
}
}
위 코드를 살펴보겠습니다. createImageFile() 함수를 사용해 파일을 만들어 보았습니다. 그리고 이 파일의 Uri를 카메라 앱으로 전달해야 합니다. 이 때 안드로이드 누가 버전 이상부터는 파일의 Uri를 다른 앱으로 전송할때 그대로 노출시키면 에러가 발생합니다.
val photoUri= FileProvider.getUriForFile(
this,
"${application.packageName}.provider",
it
)
그래서 FileProvider를 사용해 파일 주소를 감싸주는 코드가 위와 같이 추가되어 있는걸 확인할 수 있습니다. FileProvider 사용하기 위해서는 AndroidManifest에 추가해주어야합니다.
<application
...
<activity android:name=".StoreActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 사진 촬영을 위한 provider -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.tistory.blackjin.storeapplicaion.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
여기서 봐두어야 할건 두가지 입니다.
android:authorites : 사용할 도메인 주소를 적으면 됩니다. 저는 앱의 package name을 적어 주었습니다.
meta-data 안에 있는 @xml/provider_paths
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_image" path="."/>
</paths>
content uri를 생성할때 사용할 path를 적어주시면 됩니다. 저는 공식 문서에 있는걸 그대로 적었습니다. 이 경우 파일 주소가 어떻게 바뀌는지 확인해 볼까요?
위 예제에서는 외부 저장소의 앱 전용 공간에서 파일을 생성해 주고 있습니다. 이 경우 아래와 같은 경로를 확인하실 수 있습니다.
> file:///storage/emulated/0/Android/data/{packageName}/files/Pictures/
여기에 FileProvider를 적용하게 되면
> content://com.tistory.blackjin.storeapplicaion.provider/my_name/Android/data/{packageName}/files/Pictures
이렇게 바뀌게 됩니다. contet://로 변경되며 {packageName} + {provider_paths}가 추가된 것을 볼 수 있습니다. 이를 일반화 해보면 아래와 같습니다.
> content://{Manifest 에서 설정한 provider 정보}/Android/data/{packageName}/files/Pictures
CreateImageFile 살펴보기
사진을 촬영하게 되면 촬영한 이미지를 저장하기 위한 파일을 생성해야 합니다. 현 예제에서는 외부 저장소의 앱 전용 공간에 파일을 생성해 보겠습니다. 앞선 포스팅에서 보았듯이 이 경우에는 읽기/쓰기 권한이 필요하지 않으며 앱이 삭제되면 해당 파일 또한 같이 삭제됩니다. 만일 앱 삭제 여부와 상관없이 이미지를 저장하고 싶은면 외부 저장소의 공용 공간의 경로를 불러와 사용하셔야 합니다. 또한 이 경우에는 읽기/쓰기 권한이 당연히 필요하겠죠?
private fun createImageFile(): File? {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "blackJin"
//내부 저장소
//val path = cacheDir.absolutePath
//외부 앱 공간 저장소
val path = getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath
//외부 저장소 (공용 공간)
//val path = Environment.getExternalStorageDirectory().absolutePath
val storageDir = File(path)
if (!storageDir.exists()) {
storageDir.mkdirs()
}
Timber.d("path 경로 : $path")
try {
val tempFile = File.createTempFile(imageFileName, ".jpg", storageDir)
Timber.d("createImageFile 경로 : ${tempFile.absolutePath}")
return tempFile
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
우리는 3가지 저장소에 대해 알아봤습니다. 그럼 각 저장소 별로 파일을 생성 했을 때를 다시 한번 보겠습니다.
1. 내부저장소
내부저장소는 FileProvider로 감쌀 경우 java.lang.IllegalArgumentException 으로 크러시가 발생합니다.
2. 외부 앱 공간 저장소
이 경우 정상적으로 동작하며 역시 권한을 필요로 하지 않습니다. 왜? 앞서 말했듯이 외부 저장소 일지라도 공용 공간이 아닌 전용 공간일 경우 권한을 필요로 하지 않습니다.
3. 외부 저장소 (공용 공간)
이 경우에는 권한을 받아와야 합니다. 왜? 공용 공간에 파일을 생성 했기 때문이죠!
> 외부 저장소를 사용하는 공간 부터 Scoped Storage 정책에 맞게 작업을 해주어야합니다. 이는 다음 포스팅 때 이어서 진행하겠습니다.
사진 가져오기
마지막으로 onActivityResult에서 사진을 가져오는 코드를 보겠습니다.
if (requestCode == PICK_FROM_CAMERA) {
//카메라 에서는 intent,data == null 입니다.
setBitmap(
BitmapFactory.decodeFile(tempFile?.absolutePath)
)
}
전역 변수인 tempFile에 생성한 파일 경로를 저장했기 때문에 카메라로 찍은 이미지는 이 tempFile에 저장됩니다. 그럼 onActivityResult에서 tempFile의 경로를 불러와 비트맵으로 변환해 화면에 이미지를 보여줄 수 있습니다.
'안드로이드' 카테고리의 다른 글
[Dagger] Dagger step4 - Module 초기화 (0) | 2020.02.11 |
---|---|
카메라 예제와 함께 보는 Scoped Storage (안드로이드 Q 대응) (1) | 2020.01.13 |
[Android] Customing BottomNavView (0) | 2019.11.27 |
[Android, BottomSheet] 안드로이드 버텀 시트 사용 (0) | 2019.11.24 |
카메라 예제와 함께 보는 Scoped Storage (저장소의 종류) (6) | 2019.11.15 |