[Android] Content URI와 File URI의 차이점, File Provider와 Content URI를 활용하여 카메라 이미지 가져오기

Falco·2023년 3월 14일
0

Android

목록 보기
42/55

Problem

갤러리 및 카메라에서 이미지를 가져와 화면에 뛰우는 기능 개발 중 File URI를 활용하면 안되며 Content URI를 활용하라고 한다.

URI의 차이점은 무엇인가?

File URI

파일 시스템의 파일에 직접 참조하기 위한 URI이다. 이는 file://로 시작하며 파일 경로를 의미한다. 앱은 파일 시스템에서 직접 파일을 열 수 있다.

Content URI

Content URI는 안드로이드 4대 구성요소 중 하나인 content provider를 통해 데이터를 가져오기 위한 URI이다.
Content Provider는 안드로이드 앱 간의 데이터 공유를 가능하게 해준다.

이는 content://로 시작한다. 예를 들어 저장한 연락처 데이터에 대한 Content URIcontent://com.android.contracts/contracts와 같이 표현될 수 있다.

File URI는 외부에 노출하면 안된다.

일반적으로 타 앱과 데이터를 공유하려면 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="사진 찍기")
}

관련된 소스는 해당 Github프로젝트에서 확인 가능

profile
강단있는 개발자가 되기위하여

0개의 댓글