Bug and crash fixes (#132)
* feat(EPUB): adapt status bar icon visibility to reader theme
Pass the reader's active isDarkTheme state to EpubReaderSystemUiController to ensure status bar icons automatically switch between light and dark to remain visible against the selected background/page color.
* Implement storage tracking and optimize cache management
This commit introduces a `StorageTracker` to monitor disk usage and implements several optimizations to prevent cache bloat and handle orphaned files.
* refactor: unify book extraction paths and fix cache leaks
- Replaced random UUID and title-based hashing with stable `bookId` for extraction directories across Epub, Mobi, and FB2 parsers.
- Standardized all temporary extraction paths to `cache/imported_file_${bookId}`.
- Updated MainViewModel and MetadataExtractionWorker to provide the mandatory `bookId` during parsing.
- Ensured `clearImportedFileCache` successfully targets the correct directories on book deletion.
- Added a legacy cleanup task in `sweepOrphanedCache` to reclaim storage from old `extracted_epubs` folders.
* fix(pdf): handle NoClassDefFoundError for PdfPasswordException
Updated the PDF loading logic in PdfViewerScreen to catch Throwable
instead of Exception. This prevents the app from crashing when the
underlying pdfium library attempts to throw a PdfPasswordException
that is missing from the runtime classpath (NoClassDefFoundError).
* fix(tts): set language before voice to prevent variant reset
* EpubReaderScreen: increase scroll-hide ignore duration for tap toggles
Increase the threshold for ignoring tap toggles after bars are hidden by scrolling from 250ms to 400ms. This prevents accidental UI toggles caused by "sloppy taps" immediately following a scroll action.
* fix: resolve large bitmap crash on tall PDF pages
- Caps base layer bitmap dimensions to 3000px to prevent GPU texture limit crashes.
- Enables tiled rendering for large pages even at 1x zoom to maintain sharpness.
- Optimizes PdfBitmapPool to support rectangular bitmap allocations.
* Common.kt: ensure unique keys in voice list
* fix: handle ActivityNotFoundException in folder sync screen
* Improve file deletion logic and fix chapter index bounds in epub reader
* fix: ensure WebView bridge callbacks run on UI thread
This resolves a WebViewMethodCalledOnWrongThreadViolation in vertical mode that was preventing chapter chunks from loading beyond the initial viewport.
This commit is contained in:
parent
0d37bcefc3
commit
355664fbcc
16 changed files with 502 additions and 280 deletions
|
|
@ -270,19 +270,21 @@ internal object PdfBitmapPool {
|
|||
private val pool = ConcurrentLinkedQueue<Bitmap>()
|
||||
private const val MAX_POOL_SIZE = 4
|
||||
|
||||
fun get(size: Int): Bitmap {
|
||||
fun get(width: Int, height: Int): Bitmap {
|
||||
val iterator = pool.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val b = iterator.next()
|
||||
if (b.width == size && b.height == size && !b.isRecycled) {
|
||||
if (b.width == width && b.height == height && !b.isRecycled) {
|
||||
iterator.remove()
|
||||
b.eraseColor(AndroidColor.TRANSPARENT)
|
||||
return b
|
||||
}
|
||||
}
|
||||
return createBitmap(size, size)
|
||||
return createBitmap(width, height)
|
||||
}
|
||||
|
||||
fun get(size: Int): Bitmap = get(size, size)
|
||||
|
||||
fun recycle(bitmap: Bitmap) {
|
||||
if (!bitmap.isRecycled && pool.size < MAX_POOL_SIZE) {
|
||||
pool.offer(bitmap)
|
||||
|
|
@ -1063,7 +1065,8 @@ internal fun PdfPageComposable(
|
|||
isScrolling,
|
||||
virtualPage
|
||||
) {
|
||||
if (effectiveScale <= 1f) {
|
||||
val needsTiling = effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000
|
||||
if (!needsTiling) {
|
||||
if (tiles.isNotEmpty()) {
|
||||
val oldTiles = tiles
|
||||
tiles = emptyList()
|
||||
|
|
@ -3303,19 +3306,17 @@ internal fun PdfPageComposable(
|
|||
actualBitmapHeightPx = scaledHeight
|
||||
currentPageRotation = 0
|
||||
|
||||
val bitmap = PdfBitmapPool.get(maxOf(scaledWidth, scaledHeight))
|
||||
val MAX_BASE_DIMEN = 3000
|
||||
var baseW = scaledWidth
|
||||
var baseH = scaledHeight
|
||||
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
|
||||
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
|
||||
baseW = (baseW * downScale).toInt().coerceAtLeast(1)
|
||||
baseH = (baseH * downScale).toInt().coerceAtLeast(1)
|
||||
}
|
||||
|
||||
bitmap.eraseColor(android.graphics.Color.WHITE)
|
||||
|
||||
val finalBitmap =
|
||||
if (bitmap.width != scaledWidth || bitmap.height != scaledHeight) {
|
||||
val scaled = bitmap.scale(scaledWidth, scaledHeight, false)
|
||||
if (scaled !== bitmap) PdfBitmapPool.recycle(bitmap)
|
||||
scaled.eraseColor(android.graphics.Color.WHITE)
|
||||
scaled
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
val finalBitmap = PdfBitmapPool.get(baseW, baseH)
|
||||
finalBitmap.eraseColor(android.graphics.Color.WHITE)
|
||||
|
||||
val old = bitmapState
|
||||
if (old != null && old !== finalBitmap) {
|
||||
|
|
@ -3324,10 +3325,6 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
}
|
||||
bitmapState = finalBitmap
|
||||
|
||||
if (old != null && old !== finalBitmap) {
|
||||
if (old !== PdfThumbnailCache.get(pageIndex)) old.recycle()
|
||||
}
|
||||
currentRenderedPageId = targetPageId
|
||||
|
||||
isLoadingPage = false
|
||||
|
|
@ -3375,15 +3372,24 @@ internal fun PdfPageComposable(
|
|||
return@withContext null
|
||||
}
|
||||
|
||||
val MAX_BASE_DIMEN = 3000
|
||||
var baseW = scaledWidth
|
||||
var baseH = scaledHeight
|
||||
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
|
||||
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
|
||||
baseW = (baseW * downScale).toInt().coerceAtLeast(1)
|
||||
baseH = (baseH * downScale).toInt().coerceAtLeast(1)
|
||||
}
|
||||
|
||||
Timber.d(
|
||||
"Rendering page $pageIndex at ${scaledWidth}x${scaledHeight}"
|
||||
"Rendering page $pageIndex at ${baseW}x${baseH} (logical: ${scaledWidth}x${scaledHeight})"
|
||||
)
|
||||
val newBitmap = createBitmap(scaledWidth, scaledHeight)
|
||||
val newBitmap = createBitmap(baseW, baseH)
|
||||
localBitmap = newBitmap
|
||||
page.renderPageBitmap(
|
||||
newBitmap,
|
||||
0, 0,
|
||||
scaledWidth, scaledHeight,
|
||||
baseW, baseH,
|
||||
true
|
||||
)
|
||||
page.close()
|
||||
|
|
@ -3892,7 +3898,8 @@ private fun PdfBitmapLayer(
|
|||
filterQuality = androidx.compose.ui.graphics.FilterQuality.High
|
||||
)
|
||||
|
||||
if (effectiveScale > 1f) {
|
||||
val needsTiling = effectiveScale > 1f || targetWidth > 3000 || targetHeight > 3000
|
||||
if (needsTiling) {
|
||||
tiles.forEach { tile ->
|
||||
if (!tile.bitmap.isRecycled) {
|
||||
drawImage(
|
||||
|
|
|
|||
|
|
@ -3215,8 +3215,11 @@ fun PdfViewerScreen(
|
|||
|
||||
Timber.i("PDF document loaded optimistically. Total Pages: $totalPages.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.javaClass.name.contains("PasswordException") || e.cause?.javaClass?.name?.contains("PasswordException") == true) {
|
||||
} catch (e: Throwable) {
|
||||
val errorString = e.toString()
|
||||
val causeString = e.cause?.toString() ?: ""
|
||||
|
||||
if (errorString.contains("PasswordException") || causeString.contains("PasswordException")) {
|
||||
Timber.w("PDF is password protected or password incorrect.")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (documentPassword != null) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue