Repository navigation
Fix devotional download priority and loading/Retry UI - #308
Conversation
📱 Preview buildsSigned release builds of
Or open https://43b9bc72-alkitab-pr.yukuku.workers.dev on an Android device. These share their application IDs and signature with the Play Store builds, so installing one replaces the corresponding installed app (data is kept). This comment tracks the latest build for this PR; earlier builds keep their own URLs. |
| package yuku.alkitab.base.ac; | ||
|
|
||
| import android.content.Intent; | ||
| import android.content.res.ColorStateList; |
There was a problem hiding this comment.
rewrite Alkitab/src/main/java/yuku/alkitab/base/ac/DevotionActivity.java to kotlin, but do not remove existing comments.
There was a problem hiding this comment.
Converted the activity to Kotlin and preserved all ten existing comments verbatim. Java-facing intent factories and enum helpers remain available. Kotlin reserves Enum.name, so the API/database identifier is sourceName; values such as me-en remain unchanged. Reader lifecycle, selection, language, and theme tests pass.
| override fun createRequest(key: DevotionDownloader.Key, refresh: Boolean): DevotionDownloader.Request { | ||
| val request = Request.Builder().url(url(key)) | ||
| if (refresh) request.cacheControl(CacheControl.FORCE_NETWORK) | ||
| val call = client().newCall(request.build()) | ||
| return object : DevotionDownloader.Request { | ||
| override fun cancel() = call.cancel() | ||
|
|
||
| override fun execute(): DevotionDownloader.State { | ||
| try { | ||
| if (!refresh) { | ||
| cached(key)?.let { | ||
| return if (it.readyToUse) DevotionDownloader.State.READY else DevotionDownloader.State.UNAVAILABLE | ||
| } | ||
| } | ||
| val output = call.execute().use { response -> | ||
| if (!response.isSuccessful) throw IOException("Devotion HTTP ${response.code}") | ||
| response.body.string() | ||
| } | ||
| if (call.isCanceled()) throw IOException("Devotion cancelled") | ||
| val kind = DevotionActivity.DevotionKind.getByName(key.name) | ||
| ?: throw IOException("Unknown devotion source ${key.name}") | ||
| val article = kind.getArticle(key.date) | ||
| article.fillIn(output) | ||
| // An unsuccessful refresh must leave an offline reading intact. | ||
| if (article.readyToUse || cached(key)?.readyToUse != true) store(article) | ||
| if (!article.readyToUse) return DevotionDownloader.State.UNAVAILABLE | ||
| AppEvents.emitDevotionDownloaded(key.name, key.date) | ||
| return DevotionDownloader.State.READY | ||
| } catch (e: Exception) { | ||
| AppLog.d("DevotionDownloader", "Download failed for ${key.name} ${key.date}", e) | ||
| return DevotionDownloader.State.FAILED | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
the block of code is too terse, separate them into sections with light comments on top of the sections.
There was a problem hiding this comment.
Separated the backend into storage lookup, HTTP retrieval, article parsing/cache updates, completion notification, and failure handling sections, each with a light comment. The comments describe the current behavior.
| private val foregroundExecutor: ExecutorService = Executors.newSingleThreadExecutor(), | ||
| private val prefetchExecutor: ExecutorService = Executors.newSingleThreadExecutor(), |
There was a problem hiding this comment.
Possible to use kotlin coroutine + Dispatchers ?
There was a problem hiding this comment.
Yes. The scheduler now uses CoroutineScope(SupervisorJob()) and Dispatchers.IO.limitedParallelism(2), with at most one foreground and one prefetch coroutine. Requests remain deduplicated across both lanes. Obsolete foreground work cancels the underlying OkHttp call, since execute() is blocking; the next selection starts after that call releases its lane. Tests inject controlled CoroutineDispatchers, and a blocked-prefetch test exercises the same two-slot IO dispatcher.
| init { | ||
| executor.submit(::downloadLoop) | ||
| private class Work(val key: Key, val refresh: Boolean) { | ||
| var request: Request? = null |
There was a problem hiding this comment.
Do not use default parameters
There was a problem hiding this comment.
Removed default constructor parameters from both DevotionDownloader and DevotionDownloadBackend. Constructors require explicit dependencies; create() factories supply production wiring, and tests pass their dependencies explicitly.
| DevotionActivity.DevotionKind.RH -> ArticleRenunganHarian(date, body, readyToUse) | ||
| DevotionActivity.DevotionKind.SH -> ArticleSantapanHarian(date, body, readyToUse) | ||
| DevotionActivity.DevotionKind.ME_EN -> ArticleMorningEveningEnglish(date, body, true) | ||
| DevotionActivity.DevotionKind.ME_EN -> ArticleMorningEveningEnglish(date, body, readyToUse) |
There was a problem hiding this comment.
why is this changed to readyToUse?
There was a problem hiding this comment.
The server's NG response is persisted with readyToUse=false and a null body. Hardcoding true when loading Morning & Evening incorrectly turns that cached unavailable response into a ready article, then sends a null body to the renderer. Using the stored readyToUse flag preserves unavailable status and Retry when reopening the screen. The DAO tests verify NG cache round trips and readiness for every source.
| `DevotionActivity.display()` reads the local cache first. Ready articles remain readable offline; non-ready rows represent the server's `NG` response and show an unavailable message with Retry. Selecting an uncached reading schedules a download. Opening the screen or switching sources also prefetches from today forward: 15 days for most sources, 3 for Renungan Harian. Prefetch captures the source at scheduling time, skips existing cache rows, and prunes rows with `touchTime` older than 180 days. | ||
|
|
||
| `DevotionDownloader` uses two single-thread executors: one for the selected reading and one for prefetch. All queued and active requests share a registry keyed by `(source name, yyyyMMdd date)`, independent of article object equality. A queued selected reading is promoted to the foreground executor. A selected reading already active in prefetch reuses that request. Changing the selection cancels an obsolete foreground OkHttp call; its worker finishes before starting the latest selected request. Prefetch cannot occupy the foreground executor. Cancellation does not mark an obsolete selection as failed, and returning to a cancelling request restarts it after cancellation completes. | ||
|
|
||
| The backend rechecks the database before a normal download, so work queued before another cache update does not redownload it. It requests `GET /devotion/get?name={kind}&date={yyyymmdd}` using a client derived from `Connections.okHttp`, retaining the shared HTTP cache, user agent, and connect/read/write timeouts. A 30-second total call timeout bounds connection attempts and complete response-body reading. Responses close after reading. HTTP, network, parsing, and persistence exceptions end in `FAILED`; they never persist a partial reading. `NG` ends in `UNAVAILABLE` and is stored as a non-ready row. A failed or unavailable refresh preserves any ready cached reading. |
There was a problem hiding this comment.
module docs do not need to be so detailed, it would lose sync between the code and docs. Can you also edit the existing one so that it is not super detailed? just give a high-level overview.
There was a problem hiding this comment.
Rewrote the whole module document as a short high-level overview with key entry points and a brief testing note. Removed the detailed flow, state, database, timeout, and implementation descriptions so the document is easier to keep current.
A selected devotional reading can wait behind a slow active prefetch request, and network/HTTP errors or an
NGresponse leave the screen without actionable download status. The reader now gives the selected source/date a dedicated foreground coroutine, shows queued/downloading/failure/unavailable states, and offers Retry while retaining offline readings.Investigation
758793f5, Refactor DevotionDownloader to use ExecutorService instead of Thread #150). The May Kotlin conversion (5c14af08, REM-16: convert 6 Java files to Kotlin #187) preserves those behaviors. Git history does not establish the revamp as the cause of a new regression.readyToUse=truefor unavailable rows.Result
NGis separately unavailable. Retry revalidates the HTTP cache and bypasses the database, while deduplicating active work.Validation
./gradlew assemblePlainDebug testPlainDebugUnitTest --tests '*Devotion*', covering all 39 devotional tests, including native screenshot centering and app-language checks.NG, offline cache, refresh preservation, storage failure, and localhost stalled-body timeout/cancellation.DevotionActivityTestto regenerate PNGs underAlkitab/build/snapshots/devotions/.Same-repository PR targeting
developso the existing Android CI can build signed release APKs and publish PR previews. Do not merge.