Skip to content

Commit a28ec94

Browse files
committed
Reduce reader memory pressure
1 parent 3b1ebcb commit a28ec94

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

app/src/main/kotlin/com/fredapp/wbooks/transfer/UploadServer.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import com.fredapp.wbooks.util.uniqueFile
1414
import fi.iki.elonen.NanoHTTPD
1515
import kotlinx.coroutines.runBlocking
1616
import java.io.File
17+
import java.io.IOException
1718
import java.net.URLDecoder
1819
import java.net.URLEncoder
20+
import java.nio.file.Files
1921
import java.security.MessageDigest
2022

2123
/**
@@ -1095,7 +1097,7 @@ class UploadServer(
10951097
val safeName = originalName.replace(Regex("[\\\\/:*?\"<>|]"), "_")
10961098
val dest = uniqueFile(targetDir, safeName)
10971099
if (!dest.isInsideBooksDir() || dest.canonicalFile == booksDir.canonicalFile) continue
1098-
File(tempPath).copyTo(dest, overwrite = false)
1100+
moveUploadedTemp(File(tempPath), dest)
10991101
appendToOrder(targetDir, dest.name)
11001102
written++
11011103
}
@@ -1505,6 +1507,15 @@ class UploadServer(
15051507
writeOrder(dir, listOf(name) + readOrder(dir).keys.filterNot { it == name })
15061508
}
15071509

1510+
private fun moveUploadedTemp(temp: File, dest: File) {
1511+
try {
1512+
Files.move(temp.toPath(), dest.toPath())
1513+
} catch (_: IOException) {
1514+
temp.copyTo(dest, overwrite = false)
1515+
temp.delete()
1516+
}
1517+
}
1518+
15081519
private fun currentTopFolderNames(): List<String> =
15091520
booksDir.listFiles { f -> f.isDirectory }
15101521
?.map { it.name }

app/src/main/kotlin/com/fredapp/wbooks/ui/ReaderViewModel.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class ReaderViewModel(
151151
private val _document = MutableStateFlow<DocumentState>(DocumentState.Idle)
152152
val document: StateFlow<DocumentState> = _document.asStateFlow()
153153
private var loadJob: Job? = null
154+
private var cacheStoreJob: Job? = null
154155

155156
/**
156157
* Identity of the currently-open book, or null when nothing is open.
@@ -599,6 +600,7 @@ class ReaderViewModel(
599600

600601
fun openBook(book: Book) {
601602
loadJob?.cancel()
603+
cacheStoreJob?.cancel()
602604
// Reset pace baseline so the first reportPosition after the new book
603605
// loads doesn't compute a cross-book delta.
604606
lastAdvancePosition = null
@@ -681,13 +683,14 @@ class ReaderViewModel(
681683
parseBook(book).also { parsed ->
682684
updateLoadingProgress(book, 95, "Preparing reader")
683685
Log.i(TAG, "Parsed ${book.id} (${book.format}) in ${System.currentTimeMillis() - startedAt}ms")
684-
appScope.launch(Dispatchers.IO) {
686+
cacheStoreJob = appScope.launch(Dispatchers.IO) {
685687
val storeStartedAt = System.currentTimeMillis()
686688
runCatching { documentCache.store(key, parsed) }
687689
.onSuccess {
688690
Log.i(TAG, "Cached ${book.id} in ${System.currentTimeMillis() - storeStartedAt}ms")
689691
}
690692
.onFailure {
693+
if (it is kotlinx.coroutines.CancellationException) throw it
691694
Log.w(TAG, "Failed to cache ${book.id}", it)
692695
}
693696
}
@@ -735,9 +738,17 @@ class ReaderViewModel(
735738

736739
fun closeBook() {
737740
loadJob?.cancel()
741+
cacheStoreJob?.cancel()
738742
_document.value = DocumentState.Idle
739743
}
740744

745+
override fun onCleared() {
746+
loadJob?.cancel()
747+
cacheStoreJob?.cancel()
748+
sessionFlushJob?.cancel()
749+
super.onCleared()
750+
}
751+
741752
// ---- Settings edits ----
742753
fun cycleMode() = editSettings { it.copy(mode = it.mode.next()) }
743754
fun cycleFont() = editSettings { it.copy(font = it.font.next()) }

app/src/main/kotlin/com/fredapp/wbooks/ui/reader/BlockRendering.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
1616
import androidx.compose.ui.graphics.asImageBitmap
1717
import androidx.compose.ui.layout.ContentScale
1818
import androidx.compose.ui.platform.LocalConfiguration
19+
import androidx.compose.ui.platform.LocalDensity
1920
import androidx.compose.ui.text.AnnotatedString
2021
import androidx.compose.ui.text.SpanStyle
2122
import androidx.compose.ui.text.buildAnnotatedString
@@ -110,8 +111,9 @@ private fun ImageBlockView(
110111
val isRound = config.isScreenRound
111112
val minAxis = min(config.screenWidthDp, config.screenHeightDp).dp
112113
val safeSide = if (isRound) (minAxis * (1f / sqrt(2f))) else minAxis
113-
val bitmap = remember(block) {
114-
runCatching { BitmapFactory.decodeByteArray(block.bytes, 0, block.bytes.size) }.getOrNull()
114+
val maxBitmapPx = with(LocalDensity.current) { safeSide.roundToPx() }.coerceAtLeast(1)
115+
val bitmap = remember(block, maxBitmapPx) {
116+
decodeSampledBitmap(block.bytes, maxBitmapPx)
115117
}
116118
Box(
117119
modifier = Modifier
@@ -142,6 +144,30 @@ private fun ImageBlockView(
142144
}
143145
}
144146

147+
/**
148+
* Decode at roughly the largest size the watch can display. EPUB cover art can
149+
* easily be thousands of pixels wide; decoding those full-size bitmaps for a
150+
* 300-450 px display is wasted heap and a classic path to OOM on Wear OS.
151+
*/
152+
private fun decodeSampledBitmap(bytes: ByteArray, maxDisplayPx: Int): android.graphics.Bitmap? =
153+
runCatching {
154+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
155+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
156+
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return@runCatching null
157+
158+
var sample = 1
159+
val longest = maxOf(bounds.outWidth, bounds.outHeight)
160+
while (longest / (sample * 2) >= maxDisplayPx) {
161+
sample *= 2
162+
}
163+
164+
val options = BitmapFactory.Options().apply {
165+
inSampleSize = sample
166+
inPreferredConfig = android.graphics.Bitmap.Config.RGB_565
167+
}
168+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
169+
}.getOrNull()
170+
145171
private fun List<Run>.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
146172
for (run in this@toAnnotatedString) {
147173
val style = SpanStyle(

0 commit comments

Comments
 (0)