문제 상황
앱에서 PDF 파일을 저장하는 기능이 필요합니다. 안드로이드의 경우 버전에 따라서 필요한 권한과 사용 함수가 다르기 때문에 안드로이드 버전을 체크하고 다운로드 하는 기능을 구현해야합니다. 제 프로젝트는 minSdk가 26, targetSdk가 34이기 때문에 26 이상의 버전들에 대해서 처리해야합니다.
안드로이드 6 (API 레벨 23) 이상
- 런타임 권한 요청: 파일 시스템에 접근하기 위해 사용자로부터 스토리지 접근 권한을 런타임에 요청해야 합니다. READ_EXTERNAL_STORAGE와 WRITE_EXTERNAL_STORAGE 권한이 필요합니다.
안드로이드 10 (API 레벨 29) 이상
- 스코프드 스토리지 도입: 애플리케이션은 자신의 내부 저장소 또는 특정 유형의 파일(예: 사진, 비디오, 오디오)에 대해서만 접근할 수 있습니다. 외부 저장소에 있는 모든 파일에 대한 자유로운 접근이 제한됩니다.
- MediaStore API 사용: 공유 스토리지에 있는 미디어 파일에 접근하기 위해서는 MediaStore API를 사용해야 합니다.
코드 구현
1. 안드로이드 6 이상, 10 미만의 경우를 위해서 권한을 받습니다.
// androidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
private fun handleStoragePermissions(context: Context): MutableList<String> {
val list = mutableListOf<String>()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
list.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
return list
}
fun handlePdfPermission(
permissionLauncher: ActivityResultLauncher<Array<String>>,
context: Context
) {
val permissions = mutableListOf<String>()
// 파일 관리 권한 (Android R 이하)
permissions.addAll(handleStoragePermissions(context))
permissionLauncher.launch(permissions.toTypedArray())
}
@Composable
fun rememberPermissionLauncher(
exampleViewModel: ExampleViewModel
): ActivityResultLauncher<Array<String>> =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val allPermissionsGranted = permissions.entries.all { it.value }
exampleViewModel.updatePdfPermissionResult(
if (allPermissionsGranted) PdfPermissionState.PERMISSION_GRANTED else PdfPermissionState.PERMISSION_DENIED
)
}
enum class PdfPermissionState {
PERMISSION_DENIED,
PERMISSION_REQUIRED,
PERMISSION_GRANTED
}
2. 파일을 저장하기 전에 버전을 확인합니다.
if (exampleViewModel.pdfPermissionResult.value == PdfPermissionState.PERMISSION_GRANTED) {
// 버전 확인을 위한 분기점
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
savePdfToScopedStorage(it.byteStream(), context, fileName)
} else {
savePdfToLegacyExternalStorage(it.byteStream(), context, fileName)
}
} else {
handlePdfPermission(permissionLauncher, context)
}
3. 안드로이드 10 이상인 경우 미디어스토어 api를 이용해서 저장합니다.
@RequiresApi(Build.VERSION_CODES.Q)
private fun savePdfToScopedStorage(
inputStream: InputStream,
context: Context,
fileName: String
) {
try {
val values = ContentValues().apply {
put(MediaStore.Files.FileColumns.DISPLAY_NAME, fileName)
put(MediaStore.Files.FileColumns.MIME_TYPE, "application/pdf")
put(MediaStore.Files.FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri: Uri? = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
uri?.let {
resolver.openOutputStream(it).use { outputStream ->
inputStream.copyTo(outputStream!!)
}
}
} catch (e: IOException) {
LogUtils.e("IOException when saving PDF: ${e.message}")
} catch (e: SecurityException) {
LogUtils.e("SecurityException on file access: ${e.message}")
} catch (e: IllegalArgumentException) {
LogUtils.e("IllegalArgumentException: ${e.message}")
} catch (e: Exception) {
LogUtils.e("Exception: ${e.message}")
}
}
4. 안드로이드 10 이하인 경우, 사용자에게 권한을 받은 후에, 파일을 저장합니다.
private fun savePdfToLegacyExternalStorage(
inputStream: InputStream,
context: Context,
fileName: String
) {
try {
val downloadDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val pdfFile = File(downloadDir, fileName)
val bufferSize = 1024
val buffer = ByteArray(bufferSize)
var totalRead = 0
if (downloadDir != null) {
if (!downloadDir.exists()) {
downloadDir.mkdirs()
}
}
inputStream.use { stream ->
FileOutputStream(pdfFile).use { output ->
var readCount = stream.read(buffer)
while (readCount != -1) {
output.write(buffer, 0, readCount)
totalRead += readCount
readCount = stream.read(buffer)
}
}
}
} catch (e: IOException) {
LogUtils.e("IOException saving PDF: ${e.message}")
} catch (e: Exception) {
LogUtils.e("An unexpected error occurred: ${e.message}")
} catch (e: Exception) {
LogUtils.e("Exception: ${e.message}")
}
}
참고 링크
SDK 플랫폼 출시 노트 | Android Studio | Android Developers
Privacy changes in Android 10 | Android Developers
'질문 정리' 카테고리의 다른 글
Secure SDLC이란 무엇일까? (0) | 2024.07.03 |
---|---|
안드로이드에서 CRC32로 무결성 체크하기 (0) | 2024.07.02 |
컴포즈 블루투스 권한 없을 때 처리 (0) | 2024.06.27 |
Compose Snackbar 알림 관리를 위한 Utils (0) | 2024.06.20 |
Intent 정리: 문의하기 클릭 시 이메일 앱으로만 연결 (0) | 2024.06.19 |