이번 포스팅에서는 Camera Permission에 관해서는 다루지 않습니다. 카메라를 통해 사진을 저장하는 과정을 살펴볼 것이니 Camera Pemisson이 궁금하신 분은 링크를 통해 확인하세요

AndroidManifest.xml

<!-- 카메라 권한 -->
<uses-feature
    android:name="android.hardware.camera"
    android:required="false" />

<uses-permission android:name="android.permission.CAMERA" />

<application>
  ...
  <provider
      android:name="androidx.core.content.FileProvider"
      android:authorities="com.example.planet.provider"
      android:grantUriPermissions="true"
      android:exported="false">
      <meta-data
          android:name="android.support.FILE_PROVIDER_PATHS"
          android:resource="@xml/filepaths" />
  </provider>
</application>

filepaths.xml

res/xml/filepaths.xml 에 위치한다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-cache-path name="my_images" path="/"/>
</paths>

FileProvider를 사용하기 위해 <provider>를 설정한다. 여기서 주의해야할 점은 authorities 이다. authorities은 현재 프로젝트의 패키지명 + .provider로 설정해야한다.

Context.createImageFile()

카메라는 사진을 찍고 uri를 리턴한다. uri에 지정한 이름과 uri가 만들어질 경로를 설정하는 코드이다.

fun Context.createImageFile(): File {
    // Create an image file name
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val imageFileName = "JPEG_" + timeStamp + "_"
    val image = File.createTempFile(
        imageFileName, /* prefix */
        ".jpg", /* suffix */
        externalCacheDir      /* directory */
    )
    return image
}

실제 코드

@Composable
fun UseCamera() {
    DisposableEffect(Unit) {
        onDispose {
            val path = "/storage/emulated/0/Android/data/${BuildConfig.APPLICATION_ID}/cache/"
            val cashFile = File(path)
            val result = cashFile.allDelete()
        }
    }

    val context = LocalContext.current
    val file = context.createImageFile()
    val uri = FileProvider.getUriForFile(
        Objects.requireNonNull(context),
        BuildConfig.APPLICATION_ID + ".provider", file
    )
    var capturedImageUri by remember {
        mutableStateOf<Uri>(Uri.EMPTY)
    }


    val cameraLauncher =
        rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
            // cameraLauncher.launch(uri)를 실행하면  내부 코드 실행
            if (success) capturedImageUri = uri
        }

    val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)

    CameraButton {
        if (cameraPermissionState.status.isGranted) {
            runCatching {
                  // 카메라 촬영 시작
                  cameraLauncher.launch(uri)
              }.onFailure { error ->
                Log.d(TAG, "error: ${error.message}")
                }

        } else {
            // 권한 부여
            cameraPermissionState.launchPermissionRequest()
        }
    }
}

BuildConfig.APPLICATION_ID은 반드시 현재 사용중인 프로젝트의 패키지명으로 와야한다.
카메라를 통해 가져온 이미지는 “/storage/emulated/0/Android/data/${BuildConfig.APPLICATION_ID}/cache/” 경로에 저장된다. 사용자가 지우지 않으면 영구적으로 저장된다. 불필요한 리소스이므로 DisposableEffect{}를 통해 제거해줘야 한다.

File.allDelete()

디렉토리 또는 파일을 삭제하고 싶으면 file.delete()를 사용하면 된다. 그러나 디렉토리 내부에 데이터가 있으면 삭제가 되지않는다.
File.allDelete() 확장함수를 만들어 디렉토리 내부의 데이터를 제거해보자.

fun File.allDelete():Boolean? {
    var returnData = false
    val result = runCatching {     if (this.exists()) {
        val files = this.listFiles()
        if (files != null && files.size >0) {
            files.forEachIndexed { index, file ->
                returnData = file.delete()
            }
            return returnData
        } else {
            return false
        }
    } else {
        return false
    } }.onSuccess { return true }.onFailure { error ->
        Log.d(TAG, "error: ${error.message}")
        return false }

    return result.getOrNull()
}

CameraButton

@Composable
fun CameraButton(onClick:() -> Unit) {
    Card(
        modifier = Modifier.size(50.dp),
        shape = CircleShape,
        colors = CardDefaults.cardColors(
            contentColor = Color.Black,
            containerColor = Color.White
        ),
        elevation = CardDefaults.elevatedCardElevation(4.dp)
    ) {
        Box(
            modifier = Modifier.fillMaxSize().clickable { onClick() }
        ) {
            Icon(
                imageVector = Icons.Default.CameraAlt,
                contentDescription = null,
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

FileProvider

FileProvider가 뭔지 궁금해하시는 분이 계실것 같아서 설명하려고한다. 사실 내가 몰랐고 궁금해서 찾아봤다.

먼저 ContentProvider에 대해 알아야 할 필요가 있다. ContentProvider는 데이터를 캡슐화하여 다른 응용프로그램에 제공하는 Android 구성 요소이다. 여러 응용 프로그램간에 데이터를 공유해야하는 경우에만 필요하다.
예를 들어 연락처의 데이터를 다른 응용 프로그램과 공유하게 되는 경우에도 사용되고 이 때 ContentProvider의 하위 클래스인 ContactsProvider를 사용한다.

FileProvider는 ContentProvider의 하위 클래스이며 앱의 내부 파일을 공유하는 데 사용된다.

FileProvider를 사용하는 방법

  • AndroidManifest파일에서 FileProvider를 정의한다.
  • FileProvider가 다른 응용 프로그램과 공유 할 모든 경로가 포함된 XML 파일을 만든다.

FileProvider의 속성

AndroidManifest.xml에서 FileProvider에 대해 정의하려면 아래의 속성에 대해 숙지해야한다.

  • android:name
  • android:authorities
  • android:grantUriPermissions
  • android:exported

android:name
name에는 FileProvider가 있는 경로를 설정한다. 여기서는 “androidx.core.content.FileProvider”로 설정했다.

android:authorities
적어도 하나의 고유 권한을 반드시 정의해야한다. Android 시스템은 모든 제공자의 목록을 유지하며 권한별로 이를 구분한다. 권한은 애플리케이션 ID가 Android 애플리케이션을 정의하는 것처럼 FileProvider를 정의한다.
필자는 현재 프로젝트의 패키명 + .provider를 사용했다.
ex) “com.example.planet.provider”

android:grantUriPermissions
FileProvider를 잠긴 방으로 생각하면 이 속성은 외부 앱에 임시 일회성 키를 제공한다. FileProvider에 접근할 permission이 없을 경우 일시적으로 데이터 접근 permission을 부여 받을 수 있다. 임시 접근 권한이라고 생각하면 될 것 같다.
이 속성을 사용하면 앱의 내부 저장소를 안전하게 공유할 수 있다. android:grantUriPermissions의 값을 true로 사용하자.

android:exported
exported속성은 외부 어플리케이션의 접근 여부를 설정할 수 있다.
이 속성을 이해하려면 FileProvider를 문이 잠긴 방으로 생각해보자. 값을 true로 설정하면 기본적으로 모든 사람에게 문이 열린다. 다른 모든 앱이 권한을 부여받지 않고 FileProvider를 사용할 수 있기 때문에 큰 보안문제가 발생할 수 있다.
android:exported의 값을 false로 설정하자.

filepaths.xml 생성

FileProvider를 사용할 때 이 하위 요소를 정의해야한다. FileProvider가 외부 앱과 공유할 수 있는 모든 데이터 경로가 포함 된 XML 파일의 경로를 정의해야한다.

XML 파일에는 루트로 <paths> 요소가 있어야한다. <paths> 요소에는 다음 중 하나일 수 있는 하나 이상의 하위 요소가 있어야한다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 내부  스토리지, Context.getFilesDir() -->
    <files-path name="" path=""/>
    <!-- 내부  캐시 스토리지,Context.getCacheDir () -->
    <cache-path name="" path=""/>
    <!-- 공용 외부 저장소,  Environment.getExternalStorageDirectory() -->
    <external-path name="" path=""/>
    <!-- 외부  스토리지,  Context.getExternalFilesDir(null) -->
    <external-files-path="" path=""/>
    <!-- 외부  캐시 스토리지,  Context.getExternalCacheDir() -->
    <external-cache-path name="my_images" path="/"/>
</paths>

Reference

medium.com
Android FileProvider
File delete 레퍼런스