API 29이하에서 외부 저장소 권한 문제 해결

오리·2024년 8월 22일

상황

안드로이드 api 29 이하 버전으로 녹음시도시 앱이 크래시 된다.

Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)

api 30 이상 버전은 정상적으로 동작함.

원인

API 30부터는 Scoped Storage가 기본으로 적용되어 외부 저장소에 대한 권한 없이도 앱이 자신의 전용 디렉토리에 파일을 저장하고 접근할 수 있게 되어있음

  • Scoped Storage는 앱이 다른 앱의 파일에 접근하는 것을 제한하며, 앱 전용 디렉토리(getExternalFilesDir())를 사용하도록 권장됨

그러나 API 29에서는 Scoped Storage가 도입되지 않았고, 기존 방식의 외부 저장소 접근이 여전히 사용되었기 때문에, 외부 저장소에 접근하기 위해서는 명시적인 권한 필요

해결

  1. 안드로이드 29 이하의 버전에서 외부 저장소에 접근할 때는 명시적으로 WRITE_EXTERNAL_STORAGE권한을 요청하고, 해당 권한이 부여된 후에 파일을 생성하여 접근
@Composable
fun GetRecordAccess(
  onGranted: () -> Unit
) {
  val recordAudioPermission = Manifest.permission.RECORD_AUDIO
  val writeExternalStoragePermission = Manifest.permission.WRITE_EXTERNAL_STORAGE

  val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    arrayOf(recordAudioPermission)
  } else {
    // api 29 이하에서는 WRITE_EXTERNAL_STORAGE 권한도 필요
    arrayOf(recordAudioPermission, **writeExternalStoragePermission**)
  }

  val permissionState = remember { mutableStateOf(false) }
  val requestPermissionLauncher = rememberLauncherForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
  ) { permissionsMap ->
    permissionState.value = permissionsMap.values.all { it }
  }

  if (permissionState.value) {
    onGranted()
  } else {
    LaunchedEffect(permissionState.value) {
      requestPermissionLauncher.launch(permissions)
    }
  }
}
  1. 앱이 외부 저장소에 대한 이전 방식의 접근을 계속 사용할 수 있도록 하는 설정 추가
android:requestLegacyExternalStorage="true
  1. (추가) 권한 부족하면 토스트 메시지 추가 및 녹음 서비스에 접근 막기
@Composable
fun GetRecordAccess(
  context: Context,
  onGranted: () -> Unit,
  onDisMiss: () -> Unit = {}
) {
  val recordAudioPermission = Manifest.permission.RECORD_AUDIO
  val writeExternalStoragePermission = Manifest.permission.WRITE_EXTERNAL_STORAGE

  val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    arrayOf(recordAudioPermission)
  } else {
    // api 29 이하에서는 WRITE_EXTERNAL_STORAGE 권한도 필요
    arrayOf(recordAudioPermission, writeExternalStoragePermission)
  }

  val permissionState = remember { mutableStateOf(false) }
  val requestPermissionLauncher = rememberLauncherForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
  ) { permissionsMap ->
    val allPermissionsGranted = permissionsMap.values.all { it }
    if (allPermissionsGranted) {
      permissionState.value = true
      onGranted()
    } else {
      permissionState.value = false
      onDisMiss()
      Toast.makeText(
        context,
        "권한을 허용해주세요.",
        Toast.LENGTH_LONG
      ).show()
    }
  }

  if (permissionState.value) {
    onGranted()
  } else {
    LaunchedEffect(Unit) {
      requestPermissionLauncher.launch(permissions)
    }
  }
}

0개의 댓글