Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions app/src/main/java/com/ethran/notable/data/PageDataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -666,6 +665,16 @@ class PageDataManager @Inject constructor(
}

fun setBackground(pageId: String, background: CachedBackground) {
if (background.bitmap == null) {
val msg = "setBackground: skipping cache write, bitmap is null (id=${background.id})"
log.w(msg)
appEventBus.tryEmit(
AppEvent.GenericError(
msg
)
)
Comment on lines +669 to +675

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setBackground() emits AppEvent.GenericError when background.bitmap is null. Since GenericError is user-facing (snackbar) and setBackground() can be hit repeatedly during rendering/zoom while a file is temporarily unavailable, this risks spamming users for transient conditions. Consider downgrading this to logging (or a throttled/once-per-file ActionHint) and only emitting a user-visible error after repeated/persistent failures.

Suggested change
val msg = "setBackground: skipping cache write, bitmap is null (id=${background.id})"
log.w(msg)
appEventBus.tryEmit(
AppEvent.GenericError(
msg
)
)
log.w("setBackground: skipping cache write, bitmap is null (id=${background.id})")

Copilot uses AI. Check for mistakes.
return
}
dataScope.launch {
// we assume that the pageId is in current notebook.
val observeBg = appRepository.isObservable(pageFromDb?.notebookId)
Expand Down Expand Up @@ -749,26 +758,38 @@ class PageDataManager @Inject constructor(
val eventString = fileObserverEventNames(event)

log.d("Background file changed: $filePath [event=$eventString]")

// HANDLE DELETION / RE-REGISTRATION
if (event == DELETE || event == DELETE_SELF) {
log.d("Background file deleted.")
log.d("Background file deleted. Waiting for recreation...")
synchronized(fileObservers) {
fileObservers.remove(filePath)?.stopWatching()
}

if (!waitForFileAvailable(filePath)) {
log.w("File changed, but does not exist: $filePath")
log.w("File disappeared and did not return: $filePath")
appEventBus.tryEmit(
AppEvent.ActionHint(
"Background does not exist",
3000
)
)
return@launch
} else
} else {
// RE-REGISTER: This starts a new observer on the new file handle
observeBackgroundFile(pageId, filePath)
}
}

// IMPORTANT: Return here. Do NOT emit invalidateFileFlow.
// We wait for the CLOSE_WRITE event from the NEW observer.
return@launch
}

invalidateFileFlow.emit(filePath)
// HANDLE ACTUAL DATA UPDATES
// Only emit when the file is ready to be read (CLOSE_WRITE or MOVED_TO)
if (event == CLOSE_WRITE || event == MOVED_TO) {
log.d("File is stable. Invalidating flow for: $filePath")
invalidateFileFlow.emit(filePath)
}
Comment on lines +788 to +792

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bitmask issue for the stability gate: event == CLOSE_WRITE || event == MOVED_TO should use bitwise checks, otherwise a combined event value may skip invalidation and the background won't refresh.

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/com/ethran/notable/io/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.ethran.notable.SCREEN_HEIGHT
import com.ethran.notable.SCREEN_WIDTH
import com.ethran.notable.utils.logCallStack
import com.onyx.android.sdk.utils.UriUtils.getDataColumn
import io.shipbook.shipbooksdk.Log
import io.shipbook.shipbooksdk.ShipBook
import kotlinx.coroutines.delay
import java.io.File
Expand Down Expand Up @@ -256,12 +257,14 @@ suspend fun waitForFileAvailable(
var count = 1
while (System.currentTimeMillis() - start < timeoutMs) {
if (file.exists() && file.length() > 0) {
Log.d("waitForFileAvailable", "File available: $filePath, size=${file.length()}")
return true
}
delay(intervalMs)
intervalMs += count * count // Quadratic growth
count++
}
Log.d("waitForFileAvailable", "File not available: $filePath")
return false
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/ethran/notable/io/renderFromFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ fun loadBackgroundBitmap(filePath: String, pageNumber: Int, scale: Float): Bitma
renderPdfPageMuPdf(filePath, pageNumber, targetWidth.toInt(), resolutionModifier = 1.5f)
else
renderPdfPageAndroid(file, pageNumber, targetWidth.toInt(), resolutionModifier = 1.2f)
timer.end("loaded background")
timer.end("loaded background, newBitmap: $newBitmap")

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timing.end()'s label is now interpolating the Bitmap object ($newBitmap). This will produce a different log line every time (and usually only an object id), which makes timing logs harder to aggregate/grep and adds avoidable string allocation. Keep the timing label stable (e.g., "loaded background") and log bitmap details (size/config/isRecycled/null) in a separate log statement if needed.

Copilot uses AI. Check for mistakes.
return newBitmap
}
Loading