갤러리 및 카메라에서 이미지를 가져와 화면에 뛰우는 기능 개발 중 File URI
를 활용하면 안되며 Content URI
를 활용하라고 한다.
두 URI
의 차이점은 무엇인가?
파일 시스템의 파일에 직접 참조하기 위한 URI
이다. 이는 file://
로 시작하며 파일 경로를 의미한다. 앱은 파일 시스템에서 직접 파일을 열 수 있다.
Content URI
는 안드로이드 4대 구성요소 중 하나인 content provider
를 통해 데이터를 가져오기 위한 URI
이다.
Content Provider
는 안드로이드 앱 간의 데이터 공유를 가능하게 해준다.
이는 content://
로 시작한다. 예를 들어 저장한 연락처 데이터에 대한 Content URI
는 content://com.android.contracts/contracts
와 같이 표현될 수 있다.
일반적으로 타 앱과 데이터를 공유하려면 File URI
를 통해 앱내 이미지에 대한 접근을 허용하였다. 하지만 안드로이드 7,0
부터는 앱 외부에서 file://
와 같은 File URI
의 노출을 금지하는 StrictMode API
보안 정책을 적용 및 호환상의 이유로 앱에서 파일 경로를 공유하는 것이 금지되었다.
이에따라 안드로이드에서는 Content URI
를 활용하여 앱 내 데이터를 내보내고 이에 대한 임시 엑세스 권한을 부여하여 파일을 공유해야 한다. 이렇게 진행하면 권한 관리 및 안드로이드 시스템에서 제공하는 보안기능을 모두 활용할 수 있다.
일단 퍼미션을 허용해 주고
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
갤러리를 실행시키는 인텐트를 실행하되,
API 레벨 31(Android 12)
이전의 경우는 startActivityForResult()
및 onActivityReuslt
를 활용하여 인텐트에 대한 결과값을 수신하였지만 이는 Deprecated 되었으며 더 이상 사용을 권장하지 않는다.
대신에, registerForActivityResult() API
를 사용하여 더 쉽고 유연한 방식으로 액티비티 결과를 처리할 수 있다.
private val pickImage = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
// 이미지를 사용하는 코드 작성
}
}
fun pickImageFromGallery() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
pickImage.launch(intent)
}
Compose에서 액티비티나 프래그먼트를 대신 호출하고 그 결과를 처리하는 데 rememberLauncherForActivityResult
를 활용한다.
contract
: 인텐트로 어떤 행동을 할지 정의한다.
onResult
: 결과를 처리하는 데 사용된다.
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri: Uri? -> /* 처리 로직 */ }
)
Button(onClick = { launcher.launch("image/*") }) {
Text(text = "Select an image")
}
위의 소스는 모두
File Uri
의 형태가 아니라Content Uri
의 형태로 결과값을 수신한다.
content://com.android.providers.media.documents/document/image%3A1000000046
갤러리 앱의 content URI를 공유받음
카메라로 이미지를 촬영하고 이를 공유받는 것은 ActivityResultContract.TakePictrue()
클래스를 활용하여 구현할 수 있다.
private lateinit var outputFileUri: Uri?
val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { result ->
if (result) {
// 사진이 성공적으로 촬영되었을 때 작업 수행
// outputFileUri에 촬영된 URI값이 들어간다.
} else {
// 사진 촬영 실패
}
}
// 카메라를 실행하고 사진을 촬영하기
takePicture.launch(outputFileUri)
하지만 여기서 문제가 생긴다.
outputFileUri
는 캡처된 이미지가 저장될 위치를 나타내는 Uri
객체이다. 외부에서 내 어플리케이션 Uri
객체에 이미지가 저장되어야 하기 때문에 공유가 가능한 Content URI
를 생성해야 한다.
이를 위해 File
객체를 만들고 FileProvider
클래스를 사용하여 Uri
를 전달하여 파일에 액세스할 수 있는 권한을 보장해야 한다.
val directory = File(context.cacheDir, "images")
directory.mkdirs() // 임시 파일이 위치할 폴더를 생성한다.
val file = File.createTempFile(
"selected_image",
".jpg",
directory,
) // 해당 폴더에 임시 파일을 만든다.
val authority = context.packageName + ".fileprovider" //
val outputFileUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(
context,
authority,
file
)
} else {
Uri.fromFile(file)
}
FileProvider를 활용하기 위해서는 다음과 같이 manifest
에 선언이 필요하다.
<application>
<!-- ... -->
<!-- FileProvider 정의 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.myapp.provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- ... -->
</application>
위의 예제에서는 com.example.myapp.provider
라는 authority
값을 가진 FileProvider
를 정의하고 있다.
즉 com.example.myapp.provider/directory/files...
경로에 대한 모든 파일을 공유할 수 있다.
grantUriPermissions
속성은 Uri
에 대한 앱 간 권한 부여를 허용하는데, 이 속성을 true로 설정해야 한다.
exported
속성은 FileProvider
에 대한 외부 액세스를 허용하는데, 이 속성을 false
로 설정하여 앱 내부에서만 사용할 수 있도록 설정한다.
meta-data
요소는 FileProvider
가 사용할 파일 경로를 정의하는 데 사용된다.file_paths.xml
파일을 로드하여 FileProvider
가 사용할 파일 경로를 정의할 수 있다.
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/" />
<cache-path name="my_cache" path="cache/" />
</paths>
files-path
는 사용할 파일 경로를, cache-path
는 사용할 캐쉬 경로를 나타낸다.
따라서 결과로 공유되는
outputUri
, 즉Content URI
의 총 형태는 다음과 같다.
com.example.myapp.provider/images/selected_image_난수.jpg
컴포즈에서도 동일하게 사용이 가능하다.
val uri by remember {
MutableStateOf(FileProvider.getUriForFile(context, authority, file))
}
val takePhotoFromCameraLauncher = // 카메라로 사진 찍어서 가져오기
rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
if(it) {
// 카메라 촬영이 성공했을 때
// Handle uri
}
}
Button(onClick = {
takePhotoFromCameraLauncher.launch(uri)
}) {
Text(text="사진 찍기")
}