질문 정리

안드로이드 PDF 파일 저장하기 구현

five2week 2024. 6. 28. 19:38

문제 상황

앱에서 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

 

SDK 플랫폼 출시 노트  |  Android Studio  |  Android Developers

SDK Manager의 SDK Platforms 탭에서 다운로드할 수 있는 SDK 패키지에 관한 출시 정보를 확인하세요.

developer.android.com

Privacy changes in Android 10  |  Android Developers

 

Android 10의 개인정보 보호 변경사항  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android 10의 개인정보 보호 변경사항 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 10 (API 수준 29

developer.android.com