정상에서 IT를 외치다

카메라 예제와 함께 보는 Scoped Storage (이미지 가져오기) 본문

안드로이드

카메라 예제와 함께 보는 Scoped Storage (이미지 가져오기)

Black-Jin 2020. 1. 13. 16:47
반응형

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


지난 저장소 관련 포스팅에 이어 기존 저장소 방식에서는 어떻게 이미지를 가져오고 사용하는지 살펴보겠습니다.


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


권한 가져오기

저장소의 종류

이미지 가져오기

안드로이드 Q 대응




갤러리에서 사진 가져오기


갤러리에서 사진을 가져올 때는 아무 권한도 필요 없습니다. 그저 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의 경로를 불러와 비트맵으로 변환해 화면에 이미지를 보여줄 수 있습니다.


반응형
Comments