diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt index b0b6fcb307..203df46aa3 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt @@ -9,6 +9,9 @@ package io.element.android.libraries.mediaviewer.api.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -25,20 +28,28 @@ class PdfRendererManager( ) { private val mutex = Mutex() private var pdfRenderer: PdfRenderer? = null - private val mutablePdfPages = MutableStateFlow>(emptyList()) - val pdfPages: StateFlow> = mutablePdfPages + private val mutablePdfPages = MutableStateFlow>>(AsyncData.Uninitialized) + val pdfPages: StateFlow>> = mutablePdfPages fun open() { coroutineScope.launch { mutex.withLock { withContext(Dispatchers.IO) { - pdfRenderer = PdfRenderer(parcelFileDescriptor).apply { - // Preload just 3 pages so we can render faster - val firstPages = loadPages(from = 0, to = 3) - mutablePdfPages.value = firstPages - val nextPages = loadPages(from = 3, to = pageCount) - mutablePdfPages.value = firstPages + nextPages - } + runCatching { + PdfRenderer(parcelFileDescriptor) + }.fold( + onSuccess = { pdfRenderer -> + this@PdfRendererManager.pdfRenderer = pdfRenderer + // Preload just 3 pages so we can render faster + val firstPages = pdfRenderer.loadPages(from = 0, to = 3) + mutablePdfPages.value = AsyncData.Success(firstPages.toImmutableList()) + val nextPages = pdfRenderer.loadPages(from = 3, to = pdfRenderer.pageCount) + mutablePdfPages.value = AsyncData.Success((firstPages + nextPages).toImmutableList()) + }, + onFailure = { + mutablePdfPages.value = AsyncData.Failure(it) + } + ) } } } @@ -47,7 +58,7 @@ class PdfRendererManager( fun close() { coroutineScope.launch { mutex.withLock { - mutablePdfPages.value.forEach { pdfPage -> + mutablePdfPages.value.dataOrNull()?.forEach { pdfPage -> pdfPage.close() } pdfRenderer?.close() diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt index 014efdad80..1185a2cef4 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt @@ -28,13 +28,19 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.roundToPx import io.element.android.libraries.designsystem.text.toDp +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import me.saket.telephoto.zoomable.zoomable +import java.io.IOException @Composable fun PdfViewer( @@ -59,7 +65,7 @@ fun PdfViewer( } val pdfPages = pdfViewerState.getPages() PdfPagesView( - pdfPages = pdfPages.toImmutableList(), + pdfPages = pdfPages, lazyListState = pdfViewerState.lazyListState, ) } @@ -67,6 +73,48 @@ fun PdfViewer( @Composable private fun PdfPagesView( + pdfPages: AsyncData>, + lazyListState: LazyListState, + modifier: Modifier = Modifier, +) { + when (pdfPages) { + is AsyncData.Uninitialized, + is AsyncData.Loading -> Unit + is AsyncData.Failure -> PdfPagesErrorView( + pdfPages.error, + modifier, + ) + is AsyncData.Success -> PdfPagesContentView( + pdfPages = pdfPages.data, + lazyListState = lazyListState, + modifier = modifier + ) + } +} + +@Composable +fun PdfPagesErrorView( + error: Throwable, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = buildString { + append(stringResource(id = CommonStrings.error_unknown)) + append("\n\n") + append(error.localizedMessage) + }, + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyLgRegular, + ) + } +} + +@Composable +private fun PdfPagesContentView( pdfPages: ImmutableList, lazyListState: LazyListState, modifier: Modifier = Modifier, @@ -117,3 +165,11 @@ private fun PdfPageView( } } } + +@PreviewsDayNight +@Composable +internal fun PdfPagesErrorViewPreview() = ElementPreview { + PdfPagesErrorView( + error = IOException("file not in PDF format or corrupted"), + ) +} diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt index c27b36554a..72eb73c301 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt @@ -19,6 +19,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import io.element.android.libraries.architecture.AsyncData +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import me.saket.telephoto.zoomable.ZoomableState import me.saket.telephoto.zoomable.rememberZoomableState @@ -35,10 +37,10 @@ class PdfViewerState( private var pdfRendererManager by mutableStateOf(null) @Composable - fun getPages(): List { + fun getPages(): AsyncData> { return pdfRendererManager?.run { pdfPages.collectAsState().value - } ?: emptyList() + } ?: AsyncData.Uninitialized } fun openForWidth(maxWidth: Int) {