안드로이드 개발일기
[Android] PDF 로드 및 관리 with Compose 본문
1. 파일 선택기로 pdf 파일 가져오기
앱은 사용자에게 GET_CONTENT로 파일을 로드하고 pdf를 가져오도록 기능을 제공해야 합니다.
아래는 ManagedActivityResultLauncher<Intent, ActivityResult>를 사용하여 액션을 수행하고 결과 값으로 pdf를 가지고 반환하는 코드입니다.
val openDocumentLauncher: ManagedActivityResultLauncher<Intent, ActivityResult> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult? ->
val uri = result?.data?.data
Log.i(TAG, "pdf uri $uri")
}
...
Text(
text = "get pdf",
modifier = Modifier.onClick {
onClickAddButton(openDocumentLauncher)
}
}
...
private fun onClickAddButton(
launcher: ManagedActivityResultLauncher<Intent, ActivityResult>
) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
}
launcher.launch(intent)
}
위 코드는 Text를 클릭했을 때 사용자에게 pdf 파일을 선택하고 이를 리턴받는 구조입니다.
ManagedActivityResultLauncher<Intent, ActivityResult>를 사용하는데, input은 Intent로 전달하여 사용자에게 pdf 파일을 선택하도록 type을 지정하여 launcher를 실행합니다.
결과는 ActivityResult로 반환 받습니다. 반환 값은 openDocumentLauncher의 스코프로 전달받게 됩니다. 해당 스코프에서 uri를 확인할 수 있습니다.
스코프의 과정에서 Android11부터는 파일 임시 접근 권한을 자동으로 부여받습니다. 앱을 종료하거나 기기를 재시작하면 접근 권한을 잃을 수 있기 때문에 아래 단계에서 권한을 영구적으로 만들어야 합니다.
2. 선택한 파일에 Permission 부여
val openDocumentLauncher: ManagedActivityResultLauncher<Intent, ActivityResult> =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult? ->
val uri = result?.data?.data
////// 추가 코드 //////
if (uri != null) {
val takeFlags = result.data?.flags?.and(
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) ?: 0
context.contentResolver.takePersistableUriPermission(
uri, takeFlags
)
// viewModel에 이벤트 전송
viewModel.sendEvent(HomeContract.Event.AddPdfData(uri = uri))
}
/////////////////////
}
가져온 pdf 파일을 읽으려면 takePersistableUriPermission 을 줘야 합니다.
그렇지 않으면, pdf 파일을 openPage(i)로 읽을 수 없고, 아래 런타임 에러가 발생합니다.
java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaDocumentsProvider uri content://com.android.providers.media.documents/document/document:1000000087 from pid=9473, uid=10471 requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
위의 오류는 파일의 접근 권한이 적절히 설정되지 않았을 때 발생합니다.
Intent.ACTION_OPEN_DOCUMENT로 파일을 선택한 경우 임시적 접근 권한이 부여되는데, 영구적 권한으로 바꾸기 위해서는 takePersistableUriPermission를 사용합니다.
3. Uri를 Pdf 파일로 변환
data class PdfPageData(
val pageIndex: Int,
val bitmap: Bitmap
)
...
val bitmapList = mutableListOf<PdfPageData>()
withContext(Dispatchers.IO) {
try {
contentResolver.openFileDescriptor(uri, "r").use { parcelFileDescriptor ->
val pdfRenderer = parcelFileDescriptor?.let { PdfRenderer(it) }
for (i in startIndex until totalPage) {
pdfRenderer?.openPage(i)?.let { page ->
val bitmap =
Bitmap.createBitmap(
page.width,
page.height,
Bitmap.Config.ARGB_8888
)
page.render(
bitmap,
null,
null,
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
bitmapList.add(
PdfPageData(
pageIndex = i,
bitmap = bitmap
)
)
page.close()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
return bitmapList
파일을 읽기 위해서는 contentResolver를 사용합니다.
ParcelFileDescriptor은 파일을 open, close에 사용합니다. Pdf 파일의 각 페이지를 읽어오기 위해서는 PdfRenderer 객체를 사용해야 합니다.pdfRenderer.openPage(i)로 i번째 페이지를 읽어올 수 있습니다.
openPage(i)로 읽어온 PdfRenderer.Page를 Bitmap으로 변환하여 리스트에 저장합니다. 이렇게 pdf 파일의 페이지들을 비트맵 리스트로 만들어서 화면에 로드할 것입니다.
번외. 페이지 외 Pdf 파일 정보 읽어오기
1. 파일 이름 가져오기
private fun getFileName(uri: Uri, contentResolver: ContentResolver): String? {
var fileName: String? = null
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
fileName = if (nameIndex >= 0) cursor.getString(nameIndex) else null
}
}
return fileName
}
2. 파일 총 페이지 수 가져오기
private fun getTotalPage(uri: Uri, contentResolver: ContentResolver): Int? {
var pageCount: Int? = null
runCatching {
contentResolver.openFileDescriptor(uri, "r").use { pfd ->
val pdfRenderer = pfd?.let { PdfRenderer(it) }
pageCount = pdfRenderer?.pageCount ?: -1
}
}
return pageCount
}
체크 포인트 1. openPage는 병렬로 처리할 수 없다
왜냐하면, PdfRenderer는 동시에 열 수 있는 페이지 수가 제한되어있고, 페이지 사용이 끝난 후에는 페이지를 닫아줘야 하는데 병렬로는 해당 관리를 할 수 없기 때문입니다.
(pdf 파일이 큰 경우 빠르게 페이지들을 읽어오기 위해 코루틴으로 병렬 처리를 시도해보았는데, 런타임 에러가 발생하였습니다. 알고보니 위의 이유 때문이었습니다.)
4. HorizontalPager + AsyncImage로 화면에 로드
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PdfViewer(
modifier: Modifier,
index: Int,
pageList: List<PdfPageData>,
) {
val pagerState = rememberPagerState { pageList.size }
HorizontalPager(
state = pagerState,
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) { pagerIndex ->
val data = pageList[pagerIndex]
AsyncImage(
modifier = Modifier.fillMaxSize(),
contentDescription = "PDF Page ${data.pageIndex}",
contentScale = ContentScale.FillBounds,
model = ImageRequest.Builder(LocalContext.current)
.data(data.bitmap)
.dispatcher(Dispatchers.IO)
.placeholder(R.drawable.img_placeholder)
.error(R.drawable.error_image)
.size(Size.ORIGINAL)
.build()
)
}
}
'android' 카테고리의 다른 글
[Firebase] AppCheck 적용하기 (Play Integrity) (0) | 2024.09.07 |
---|---|
[WorkManager + Flow] doWork 후 flow의 onCompletion이 호출되지 않는다 (0) | 2024.05.03 |
[Clean Architecture] UseCase를 사용하는 이유 (0) | 2023.12.14 |
[Android] Retrofit multiple API 결합하기 (Flow combine 사용) (0) | 2023.09.12 |
[Android] RecyclerView, DiffUtil사용 시 데이터가 갱신되지 않는 이슈(DiffUtil이 데이터 변경을 판단하는 기준) (0) | 2023.07.06 |