모바일/안드로이드

Android Camera2 API에서 FileNotFoundException 이슈 해결하기

jinmc 2025. 1. 6. 15:59
반응형

문제 상황

Android 앱 개발 중 Camera2 API를 사용하여 이미지를 캡처한 후 외부 저장소에 저장하려고 했을 때, 다음과 같은 예외가 발생할 수 있습니다:

FATAL EXCEPTION: videoThread
Process: com.example.myapplication, PID: 17049
java.io.FileNotFoundException: /storage/emulated/0/thisimage.jpg: open failed: EPERM (Operation not permitted)

이 에러는 Android의 저장소 접근 정책 변화로 인해 발생하는데, 특히 Android 10(API 29) 이상에서는 Scoped Storage가 도입되면서 앱이 외부 저장소에 접근하는 방식이 제한되었습니다.

원인 분석

  1. Scoped Storage 도입
    • Android 10부터는 외부 저장소 접근이 제한됩니다.
    • Environment.getExternalStorageDirectory()와 같은 경로를 사용할 경우, 특별한 설정 없이 파일을 저장하거나 읽을 수 없습니다.
  2. 권한 문제
    • WRITE_EXTERNAL_STORAGE 권한이 제대로 요청되지 않았거나, 사용자가 권한 요청을 거부했을 수 있습니다.
  3. 잘못된 경로 사용
    • /storage/emulated/0/ 경로는 앱 전용 디렉터리가 아니므로, Scoped Storage 정책에 의해 접근이 제한될 수 있습니다.

해결 방법

1. 앱 전용 디렉터리 사용

앱 전용 디렉터리는 특별한 권한 없이 사용할 수 있습니다. 다음과 같이 앱의 외부 저장소 전용 디렉터리를 사용하면 문제를 해결할 수 있습니다:

val file = File(getExternalFilesDir(null), "thisimage.jpg")
  • getExternalFilesDir(null)은 앱 전용 디렉터리를 반환합니다.
  • 이 디렉터리에서 저장된 파일은 앱이 삭제되면 함께 삭제됩니다.

2. Scoped Storage 설정 (Android 10 이상)

Android 10 이상에서 외부 저장소를 사용하려면 MediaStore를 사용하는 것이 권장됩니다. 다음은 예시 코드입니다:

val contentValues = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "thisimage.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}

val resolver = contentResolver
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.let {
    val outputStream = resolver.openOutputStream(it)
    outputStream?.use { stream ->
        stream.write(bytes)
    }
    Toast.makeText(this, "Image saved to Pictures", Toast.LENGTH_SHORT).show()
}

3. Legacy External Storage 설정 (임시 방편)

Android 10(API 29)에서는 requestLegacyExternalStorageAndroidManifest.xml에 추가하여 기존 방식으로 외부 저장소를 사용할 수 있습니다:

<application
    android:requestLegacyExternalStorage="true"
    ... >
</application>

하지만, 이 설정은 Android 11(API 30)부터는 무시됩니다. 따라서 장기적으로 MediaStore를 사용하는 방식으로 전환하는 것이 좋습니다.

4. 권한 요청 확인

외부 저장소에 파일을 저장하려면 WRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGE 권한이 필요합니다. 다음 코드는 권한 요청을 수행합니다:

fun get_permissions() {
    val permissionsList = mutableListOf(
        android.Manifest.permission.CAMERA,
        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
        android.Manifest.permission.READ_EXTERNAL_STORAGE
    )

    if (permissionsList.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }) {
        ActivityCompat.requestPermissions(this, permissionsList.toTypedArray(), 101)
    }
}

최종 코드 예제

수정된 onImageAvailable 메서드는 다음과 같습니다:

override fun onImageAvailable(reader: ImageReader) {
    val image = reader.acquireLatestImage()
    val buffer = image.planes[0].buffer
    val bytes = ByteArray(buffer.remaining())
    buffer.get(bytes)

    try {
        val file = File(getExternalFilesDir(null), "thisimage.jpg")
        FileOutputStream(file).use { output ->
            output.write(bytes)
        }
        Toast.makeText(this@MainActivity, "Image captured and saved: ${file.absolutePath}", Toast.LENGTH_SHORT).show()
    } catch (e: Exception) {
        e.printStackTrace()
        Toast.makeText(this@MainActivity, "Failed to save image", Toast.LENGTH_SHORT).show()
    } finally {
        image.close()
    }
}

결론

Android의 저장소 정책 변화로 인해 기존의 방식으로 파일을 저장하는 데 어려움이 생겼지만, 위에서 설명한 방법들을 사용하면 문제를 해결할 수 있습니다. 특히, 장기적으로는 Scoped Storage에 맞는 방식을 사용하는 것이 권장됩니다. 저는 일단 앱 전용 디렉토리를 사용했더니 해결되었습니다.

반응형