Repository navigation
Refresh conversations in the background so they are not stale when the app is opened - #6745
Conversation
bb53fe7 to
c0b96dd
Compare
abc0d72 to
7d19a5c
Compare
c0b96dd to
d4f1b1b
Compare
7d19a5c to
f92c673
Compare
d4f1b1b to
a633b9c
Compare
f92c673 to
523a0fa
Compare
a633b9c to
0b93e1e
Compare
523a0fa to
6621cf4
Compare
0b93e1e to
465e035
Compare
6621cf4 to
76d0e52
Compare
465e035 to
c7917bd
Compare
76d0e52 to
fbd79d1
Compare
c7917bd to
2b6b3b8
Compare
fbd79d1 to
9b7b1a6
Compare
2b6b3b8 to
0cdcc22
Compare
9b7b1a6 to
d9a67e2
Compare
0cdcc22 to
144cd33
Compare
d9a67e2 to
ae967b3
Compare
144cd33 to
f59604f
Compare
ae967b3 to
3da4c9f
Compare
f59604f to
79b44ce
Compare
3da4c9f to
9596d4e
Compare
79b44ce to
89b3874
Compare
9596d4e to
b9aa2b8
Compare
0a1d95f to
6de9ce9
Compare
e74c3a1 to
81d9079
Compare
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe repository adds a suspending operation for conversation room-list sync and a separate operation for message catch-up. A periodic worker syncs configured accounts, with battery-saver and foreground checks, retry handling, and connected-network scheduling. The application schedules the worker. A shared context extension provides the battery-saver check used by both workers. Tests cover repository behavior, worker outcomes, cancellation, and rescheduling. Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to A failure reading the sync timestamps can skip every account's background refresh without any retry. Accounts with slow syncs may also be cancelled on every run and stay stale. Add a fallback for the timestamp read before merging. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Background refresh uses existing account permissions, but queued synchronization can race account removal, and message catch-up failures can be reported as successful completion. The exposure is bounded to accounts configured on the device; no new external entrypoint or elevated permission was identified. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
6a83f7f9-a39c-47c6-a83f-2161bf361fbf
📒 Files selected for processing (8)
app/build.gradle.ktsapp/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.ktapp/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/extensions/ContextExtensions.ktapp/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.ktapp/src/main/java/com/nextcloud/talk/jobs/ConversationsSyncWorker.ktapp/src/test/java/com/nextcloud/talk/jobs/ConversationsSyncWorkerTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
81d9079 to
40eb091
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
eccaa627-cbd1-4b95-80dd-6cdbbf33577b
📒 Files selected for processing (4)
app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/jobs/ConversationsSyncWorker.ktapp/src/test/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepositoryTest.ktapp/src/test/java/com/nextcloud/talk/jobs/ConversationsSyncWorkerTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.
📱 QA build
The QA build installs alongside a released Nextcloud app, so you can keep Downloading the file requires a GitHub account, so open this link on the |
911db4c to
ffc197a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
ff358494-90ec-4501-844e-9958d26aa0c4
📒 Files selected for processing (2)
app/src/main/java/com/nextcloud/talk/jobs/ConversationsSyncWorker.ktapp/src/test/java/com/nextcloud/talk/jobs/ConversationsSyncWorkerTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
ffc197a to
731fcc1
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
4b31d7b8-532a-45a5-b243-37fe02def3b7
📒 Files selected for processing (2)
app/src/main/java/com/nextcloud/talk/jobs/ConversationsSyncWorker.ktapp/src/test/java/com/nextcloud/talk/jobs/ConversationsSyncWorkerTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.
| var failed = 0 | ||
| for (user in longestWaitingFirst) { | ||
| val share = (deadline - SystemClock.elapsedRealtime()) / accountsLeft | ||
| if (!syncAccount(user, share.coerceAtLeast(MIN_ACCOUNT_BUDGET_MILLIS))) { |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Keep minimum account budgets within the run deadline.
If 21 accounts are configured and the first 20 each take at least 30 seconds, this minimum consumes ten minutes before account 21 starts. WorkManager then signals the worker to stop. If those attempts keep timing out, unchanged sync timestamps can put the same accounts first on every run. Limit the allocated budgets to the remaining run time, and preserve progress so accounts that were not attempted can run next time. (developer.android.com)
731fcc1 to
ca48909
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
161b2d4f-0c8b-43c2-84a9-48ee677d7d73
📒 Files selected for processing (5)
app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.ktapp/src/main/java/com/nextcloud/talk/jobs/ConversationsSyncWorker.ktapp/src/test/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepositoryTest.ktapp/src/test/java/com/nextcloud/talk/jobs/ConversationsSyncWorkerTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
getRooms launches into the repository's own scope and returns straight away, which is what the conversation list wants and what a background worker cannot use: WorkManager tears the process down the moment the worker returns, and the sync would be cut off mid-request. Add a suspend entry point that completes when the sync and the message catch-up it triggers are done. The catch-up is launched detached on the existing path and awaited on this one, so a worker cannot report success for messages it never fetched. It deliberately leaves the observed account alone. That selects what the conversation list screen shows, and a worker walking several accounts in one run must not move it. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A backgrounded app learns nothing about its conversations until a push arrives. Where push is disabled, unavailable or simply does not turn up, that means the list, the unread counts and the cached messages stay as they were until the user opens the app. Add a periodic worker that runs the conversation list sync and its message catch-up for every configured account. Accounts are synced one after another rather than at once, because each fans out into its own bounded set of message requests and running them together multiplies that into a burst on a single wake-up. It stands down in battery saver and while the app is in the foreground, where the list refreshes itself, and leaves reachability to the work request's network constraint - the connectivity flow is frozen in a process with no UI collector, and pre-checking it there is what silently disabled the message prefetch before. WorkManager's period floor is 15 minutes and Doze defers it further, so this bounds staleness rather than delivering immediacy. Scheduling keeps an existing run instead of replacing it, so an app opened often still reaches one. The battery saver check would have been copied a third time here, so it moves to a Context extension the repository and both workers share. The worker's decision is kept apart from doWork, which reaches for the application singleton to inject itself and can therefore not be driven from a unit test. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
What is worth pinning down about a background worker is when it stands down and how many accounts it touches when it does not: a guard that quietly stops working is invisible until it turns up as battery drain or request volume, long after the change that broke it. Covers battery saver standing the run down without reading a single account, every configured account being synced rather than only the current one, a failed account asking for another attempt while its neighbours still sync, the attempt cap ending the retries, and an account lookup that throws being survivable. Unit tests gain the WorkManager testing artifact, which was available to instrumented tests only. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The background worker gave each account two minutes for its whole sync, the message catch-up included. On a slow connection the catch-up alone can take longer than that - up to twenty rooms, each needing requests that may run into the 45 second HTTP timeouts - so an account whose room list was already stored got reported as failed, and the run was retried for every account. Timing only the room list fixes that, but leaves the catch-up running inline between accounts: one account's slow prefetch then delays the room lists of every account behind it, and can use up the ten minutes WorkManager grants a run before those are reached at all. The room list is what matters - unread counts, new and left conversations - while missed messages load anyway once a chat is opened. So the worker now works in two passes: it syncs the room list of every account first, under the per-account time budget, and only then catches up messages with the time the run has left. A catch-up failure is logged and no longer fails the run, since the list it follows is already stored. To allow that, syncRooms() no longer catches up itself: it returns the rooms with new messages, or null when the sync failed, and the new catchUpRooms() prefetches them. getRooms() keeps launching the catch-up in the background, so the conversation list screen behaves as before. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…Manager The room list sync returned early whenever the network monitor did not report the device as online. The conversation list screen checks that before it syncs anyway, so the early return only ever affected the background worker - the one caller that is meant to rely on its work request's network constraint instead. The two disagree on a network that is connected but not yet validated, or sits behind a captive portal: WorkManager starts the worker, while the monitor, which waits for validation, reports offline. Every account then counted as failed and the run burned its retries without sending a single request. Send the request and let it fail like any other when the network really is unusable, as ChatMessageSyncer already does for the same reason. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
… foreground The worker checked whether the app was in the foreground only when a run started. A run can take up to the ten minutes WorkManager grants it, and opening the app in that time had the conversation list sync the account it shows while the worker went on syncing the same account. The two write the same rows, and a slower, older response landing last put stale unread counts back until the next sync. Check again before each account's room list, and leave the remaining room lists to the next run once the app is in the foreground - the same stand-down a run that starts there gets. The message catch-up is not cut short with them. The run stored those rooms' latest activity with their room list already, so the conversation list's own sync no longer sees new messages in them and would not catch them up in the run's place. It can safely run alongside the screen: its update to a conversation is skipped when the stored state is newer, and overlapping catch-ups of a room are coalesced. A run that stops early leaves accounts untried, so its warning counts failures against the accounts it tried rather than all of them. The foreground guard at the start of a run was untested, while the battery saver one next to it was covered. Both it and the app coming to the foreground mid-run are now covered, driving the real process lifecycle. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…n list A failed room list sync of an account without cached conversations is reported on syncErrorFlow, so the conversation list can tell the user why it stays empty. The flow does not say which account failed, and the list's view model collects it for as long as it lives - in the background too - and turns whatever arrives into an error dialog. The background worker runs that same sync for every account. A 401 for an account whose password was changed, or a server that is down, then put an unauthorized or error dialog on the list of whichever account the user had open when they came back. Only the sync the conversation list starts for the account it shows reports there now, as the flow's documentation already promises. The worker learns of a failure through syncRooms' return value and logs it. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…caller's thread syncRooms blocks the thread it runs on: the request is awaited with blockingSingle, retried up to three times with each attempt bounded only by the 45 second HTTP timeouts, and the stored sync state is read with blockingGet. getRooms never showed it, since it runs in the repository's own IO scope, but syncRooms runs on its caller's thread - for the background worker one of the few threads of Dispatchers.Default, sized to the CPU cores and shared with the rest of the app's coroutines, which a slow server then held for minutes. Called from the main thread it would not even get that far: the request delivers its result on the main thread that blockingSingle is blocking. The worker's ordering of the accounts did the same on a smaller scale. It sorted with sortedBy on the stored time of each account's last full sync, and sortedBy calls its selector for every comparison rather than once per account - each call a blocking database query. Run syncRooms and lastFullSyncAt on the IO dispatcher, as ConversationListUpdater does around its blocking calls, so both are safe to call from any thread, and read each account's last full sync once before sorting on the values. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
… keeping it The periodic conversation sync was scheduled with KEEP, so that an app started often does not restart the period every time and so never reaches a run. KEEP also ignores the request it is handed, though: once a version of the app has scheduled the worker, a later version that changes its interval or constraints never gets them applied - existing installs keep running the old request until the app data is cleared. UPDATE keeps what KEEP was chosen for - the scheduled run and its timing stay in place - but applies the new request to it, so changes to the schedule reach existing installs. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
… be read The worker orders the accounts by the time of their last full sync, read from the database with a blocking query. Nothing caught a failure of that read, so it escaped doWork and failed the run outright: no account was synced and no retry was scheduled, all for an order that only exists to keep slow accounts from starving the others. Fall back to the accounts' own order when the read fails, and let cancellation through as before. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
syncAccounts read the accounts, ordered them and synced them in one function with three returns, over detekt's ReturnCount limit of two. Move reading the accounts, ordering them and syncing them into functions of their own, leaving syncAccounts a single when over the outcome of the read. The behaviour is unchanged. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
6e11ff4 to
298813b
Compare
A backgrounded app learns nothing about its conversations until a push arrives. Where push is disabled, unavailable, or simply does not turn up, the list, the unread counts and the cached messages stay as they were until the user opens the app. This adds the periodic background refresh iOS has had all along.
What it does
suspendsync entry point on the repository that completes when the sync and its message catch-up are done. The existinggetRoomslaunches into the repository's own scope and returns immediately, which is what the list wants and what a worker cannot use — WorkManager tears the process down the moment the worker returns.ConversationsSyncWorker, a 15-minute periodic worker, syncing every configured account one after another rather than at once.What a reviewer should push back on
isOnlinepre-check, deliberately — the connectivity flow is frozen in a process with no UI collector, which is what silently disabled the message prefetch before (fix(chat): make the push message prefetch actually run on a flaky connection #6727). Reachability is the work request'sNetworkType.CONNECTEDconstraint.ponytail:comment naming the ceiling. The delta sync from Ask the server only for the conversations that changed #6744 is what makes this affordable; cap the run and take the stalest accounts first if request volume ever shows it.Not in this PR
🚧 TODO
🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)