Skip to content

release: promote Android foundation FND-002 - #14

Merged
BeforeLights merged 21 commits into
mainfrom
dev
Aug 2, 2026
Merged

release: promote Android foundation FND-002#14
BeforeLights merged 21 commits into
mainfrom
dev

Conversation

@BeforeLights

@BeforeLights BeforeLights commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • promote the verified FND-002 Android offline-shell foundation from dev
  • include Room account/workspace isolation, WorkManager recovery ports, Keystore boundaries, bilingual/resource/network/backup checks, and process-recreation instrumentation
  • preserve the corrected FND-001 evidence SHA and orchestration checkpoint

Promotion policy

  • Feature PR feat(android): close offline shell foundation gaps #13 was merged to dev after hosted checks; CodeRabbit was intentionally skipped there.
  • This is the one CodeRabbit review target for this promotion PR. Request a full review once the hosted checks are ready; do not invoke a second review on this PR.
  • Changed files before opening: 19 (under the 280-file pre-review limit).

Verification on feature PR

  • corepack pnpm repo:check
  • corepack pnpm repo:build
  • corepack pnpm infra:check (static checks; OpenTofu unavailable)
  • Android unit, assemble/Android-test compile, and connected instrumentation on Medium_Phone(AVD) - 17
  • repository Android-shell contract tests

Release notes

  • No product requirement status is promoted to verified; FND-002 is foundation evidence only.
  • No customer data, credentials, signing keys, or original logo bytes were changed.

Summary by CodeRabbit

  • New Features
    • Drafts now persist through activity recreation and can be recovered from durable local storage.
    • Synchronization supports account- and workspace-scoped queues, retries, completion tracking, and network-aware background processing.
    • Signing out cancels pending synchronization and clears local data and device credentials.
    • Added secure device payload encryption and validation.
  • Bug Fixes
    • Improved account and workspace isolation, duplicate handling, and sign-in/sign-out lifecycle ordering.
  • Documentation
    • Added Android architecture, operations, recovery, and testing guidance.
  • Tests
    • Expanded coverage for drafts, synchronization, security, isolation, lifecycle behavior, and configuration.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@BeforeLights, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 139d2786-cda9-4531-a2df-964c3b04e941

📥 Commits

Reviewing files that changed from the base of the PR and between 001dc16 and 92b3e9a.

📒 Files selected for processing (2)
  • apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeLifecycleTest.kt
  • docs/operations/code-review-14-disposition.md
📝 Walkthrough

Walkthrough

The Android foundation now uses account/workspace-scoped Room queues, WorkManager synchronization, device-key encryption, runtime dependency wiring, and durable capture drafts. Tests and documentation cover isolation, retries, activity recreation, security boundaries, and foundation verification.

Changes

Android foundation

Layer / File(s) Summary
Scoped storage and device security
apps/android/app/build.gradle.kts, apps/android/app/schemas/..., apps/android/app/src/main/java/com/databreeze/android/storage/..., apps/android/app/src/main/java/com/databreeze/android/security/..., apps/android/app/src/androidTest/.../RoomIsolationTest.kt, apps/android/app/src/test/.../BoundaryTest.kt, apps/android/gradle/libs.versions.toml
Room storage now uses account/workspace-scoped composite queue keys, validated entities, scoped operations, schema export, and durable or in-memory implementations. Device keys support alias validation, deletion, retrieval, and AES-GCM payload encryption.
Scoped sync orchestration
apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt, apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt, apps/android/app/src/main/java/com/databreeze/android/DataBreezeApplication.kt, apps/android/app/src/main/AndroidManifest.xml, apps/android/app/src/test/.../AndroidRuntimeContractTest.kt, apps/android/app/src/test/.../SyncSchedulerContractTest.kt, tools/repo-cli/test/android-shell.test.mjs
WorkManager inputs, work names, requests, mutations, retries, rejection codes, cancellation, and worker construction now use account/workspace scope. The application supplies injected storage and transport dependencies.
Durable capture and activity recovery
apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt, apps/android/app/src/androidTest/.../MainActivityTest.kt, apps/android/README.md
Capture screens observe queued mutations and enqueue draft metadata asynchronously. The activity-recreation test verifies that draft status remains visible after recreation.
Foundation evidence and orchestration state
docs/operations/foundation-android-2026-08-02.md, docs/operations/code-review-14-disposition.md, docs/plans/execution-orchestration.json, tools/repo-cli/test/execution-orchestration.test.mjs
Android foundation evidence, review disposition, rollback notes, verified FND-002 state, and the next task identifier were updated. Repository tests validate the recorded evidence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CaptureScreen
  participant LocalStorePort
  participant SyncScheduler
  participant SyncWorker
  participant SyncTransport
  CaptureScreen->>LocalStorePort: Enqueue capture mutation
  SyncScheduler->>SyncWorker: Run scoped WorkManager task
  SyncWorker->>LocalStorePort: Load pending mutations
  SyncWorker->>SyncTransport: Send scoped request and mutations
  SyncTransport-->>SyncWorker: Return accepted, retryable, or rejected result
  SyncWorker->>LocalStorePort: Delete accepted mutations
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the promotion of the Android foundation FND-002, which matches the pull request objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (7)
apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt (1)

54-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the workspace dimension to this isolation test.

The test name states "account or workspace", but both scopes use workspace-1 and differ only by account. The workspace half of the composite key is untested here. RoomIsolationTest has the same gap: it varies only accountId and then queries a workspace that was never populated.

Add a third scope such as AccountWorkspaceScope("account-1", "workspace-2"), enqueue the same mutation-1, and assert that clear(first) leaves it intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt` around
lines 54 - 81, Extend
in_memory_store_cannot_cross_account_or_workspace_boundaries with a third scope
using the same account as first but a different workspace, enqueue its
mutation-1, and assert it remains after clear(first). Apply the same
workspace-dimension coverage to RoomIsolationTest, ensuring the queried
workspace is populated and remains isolated.
apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt (2)

185-192: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Align the fake's emission and ordering with RoomLocalStore.

Two behavior differences exist between this fake and the Room implementation:

  1. observeQueue filters after a StateFlow, so a mutation in another scope emits an unchanged list to this scope's collectors. Add distinctUntilChanged() to match Room's per-scope emission behavior.
  2. snapshotQueue returns LinkedHashMap insertion order. The DAO orders by createdAtEpochMs ASC, mutationId ASC. Once callers set real timestamps, tests that pass against the fake can fail against Room.
♻️ Proposed refactor
+import kotlinx.coroutines.flow.distinctUntilChanged
+
     override fun observeQueue(scope: AccountWorkspaceScope): Flow<List<SyncQueueEntity>> =
         updates.asStateFlow().map { values ->
-            values.filter { it.accountId == scope.accountId && it.workspaceId == scope.workspaceId }
-        }
+            values.filter { it.accountId == scope.accountId && it.workspaceId == scope.workspaceId }
+                .sortedWith(compareBy({ it.createdAtEpochMs }, { it.mutationId }))
+        }.distinctUntilChanged()
 
     override suspend fun snapshotQueue(scope: AccountWorkspaceScope): List<SyncQueueEntity> = mutex.withLock {
         items.values.filter { it.accountId == scope.accountId && it.workspaceId == scope.workspaceId }
+            .sortedWith(compareBy({ it.createdAtEpochMs }, { it.mutationId }))
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt`
around lines 185 - 192, Update observeQueue in LocalStore to apply
distinctUntilChanged after filtering so collectors emit only when their scoped
queue contents change. Update snapshotQueue to sort matching items by
createdAtEpochMs ascending, then mutationId ascending, matching RoomLocalStore’s
DAO ordering instead of relying on LinkedHashMap insertion order.

109-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the completedState parameter from markCompleted.

The Kotlin default argument works at the call site. Room receives the explicit bind parameter. However, callers can pass any String and write an invalid state. Bind SyncQueueEntity.COMPLETED_STATE directly and remove the parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt`
around lines 109 - 117, Update the Room query and markCompleted method in
LocalStore so the SET clause binds SyncQueueEntity.COMPLETED_STATE directly.
Remove the completedState parameter while preserving the existing accountId,
workspaceId, mutationId filters and return type.
apps/android/app/build.gradle.kts (1)

94-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer KSP and the Room Gradle plugin for Room 2.7.2.

Replace the kapt dependency and arguments with KSP and room { schemaDirectory(...) }. Move the existing schema file into the plugin’s variant-specific schema directory. The current $projectDir/schemas path is correct. If kapt remains, use the existing libs.plugins.kotlin.kapt alias instead of the raw plugin ID.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/build.gradle.kts` around lines 94 - 98, Replace the Room
configuration in the kapt block with KSP and the Room Gradle plugin for Room
2.7.2, using room { schemaDirectory("$projectDir/schemas") } and the appropriate
plugin aliases. Move the existing schema file into the plugin-generated
variant-specific schema directory, preserving the current projectDir/schemas
root; if kapt is still required elsewhere, apply libs.plugins.kotlin.kapt rather
than a raw plugin ID.
apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt (1)

35-51: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Import androidx.work.Data directly in AndroidRuntimeContractTest.kt. Data.Builder.build() is suitable for JVM unit tests; do not add Robolectric or move these tests to src/androidTest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt`
around lines 35 - 51, Update AndroidRuntimeContractTest to import
androidx.work.Data directly and replace the fully qualified Data references with
the imported symbol, while keeping these JVM unit tests in place and continuing
to use Data.Builder.build() without adding Robolectric.
apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json (1)

1-79: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Regenerate the Room schema output and commit it unchanged.

The identityHash matches SyncQueueEntity. Missing empty foreignKeys and views arrays do not cause MigrationTestHelper to fail, but they make the file differ from canonical Room output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json`
around lines 1 - 79, Regenerate the Room schema JSON for the database containing
SyncQueueEntity using the project’s standard Room schema export process,
ensuring the canonical empty foreignKeys and views arrays are included. Commit
the generated schema output unchanged, preserving the existing identityHash and
database definition.
tools/repo-cli/test/execution-orchestration.test.mjs (1)

179-193: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the feature pull request checkpoint.

This test does not validate ledger.checkpoint.lastFeaturePullRequest. Add an assertion for feature PR #13 so future ledger updates cannot retain the stale #12 value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/repo-cli/test/execution-orchestration.test.mjs` around lines 179 - 193,
Extend the ledger assertions in the execution orchestration test to verify that
ledger.checkpoint.lastFeaturePullRequest equals 13. Keep the existing task-state
and commit validations unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/android/app/src/androidTest/java/com/databreeze/android/MainActivityTest.kt`:
- Around line 27-33: Update MainActivityTest’s draft save verification to wait
for the saved-state semantics represented by R.string.capture_saved, rather than
merely the always-present draft-status tag. Assert that saved state before
activityRule.scenario.recreate(), then repeat the same saved-state assertion
afterward while retaining the existing screen visibility checks.

In
`@apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt`:
- Around line 33-37: Update
apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt#L33-L37
to enqueue a copy of first with workspaceId set to workspace-b and assert
snapshot("account-a", "workspace-b") returns exactly that row. Update
apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt#L54-L81 to
add AccountWorkspaceScope("account-1", "workspace-2"), enqueue the same
mutation-1 for that scope, and verify clear(first) leaves the workspace-2 row
intact, covering workspace isolation in both tests.

In `@apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt`:
- Around line 24-27: Update signOut and the SyncWorker/SyncTransport flow to
persist and enforce a revocation guard before any transport begins, so workers
that already snapshotted the queue cannot send after sign-out clears the scope
and deletes the key. Do not rely solely on syncScheduler.cancel or its
cancellation Operation; check the guard immediately before synchronization and
abort revoked work. Add a WorkManager test covering the race between queue
snapshot and signOut.

In `@apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt`:
- Around line 46-47: Update the composition root around DataBreezeApp and
CaptureScreen to pass application.runtime.syncScheduler alongside localStore and
localScope, then invoke SyncScheduler.enqueue(localScope) in the capture save
handler immediately after localStore.enqueue completes. Preserve the existing
mutation flow and ensure scheduling occurs only after the queued capture is
durable.

In
`@apps/android/app/src/main/java/com/databreeze/android/security/DeviceKeyStore.kt`:
- Around line 72-77: Update EncryptedPayload so equality and hashing compare iv
and ciphertext contents rather than ByteArray identity, either by implementing
content-based equals/hashCode or by changing it from a data class to a class
with those overrides. Preserve the existing constructor validation for the
12-byte IV and non-empty ciphertext.
- Around line 83-89: Update DeviceKeyStore.encrypt to initialize AES/GCM
encryption without a caller-supplied GCMParameterSpec, then capture the
generated IV from cipher.iv when constructing EncryptedPayload. Remove the
now-unused random generator and its import, while preserving use of the stored
payload IV during decryption.

In `@apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt`:
- Around line 56-57: Remove the default value from
SyncQueueEntity.createdAtEpochMs so callers must provide it explicitly, then
update every enqueue call site to pass the injected clock’s current millisecond
value while preserving the DAO’s chronological ordering.

In `@apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt`:
- Around line 123-135: Update doWork around SyncTransportResult.Accepted so
accepted mutations are removed from sync_queue (or purge completed rows) and the
entire batch status update/removal executes atomically in one Room transaction.
Preserve retry and rejection handling, and confirm the mutation-ID-only
SyncRequest matches the intended foundation scope without adding payload
handling unless required.
- Around line 99-103: Update the enqueueUniqueWork call in SyncPorts to replace
ExistingWorkPolicy.KEEP with an intentional policy that preserves the newest
cursor and revision, preferably APPEND_OR_REPLACE for ordered execution or
REPLACE for newest-input semantics. Keep the existing cursor and revision
request data unchanged.
- Around line 139-151: Remove the WorkManagerInitializer metadata from the
application manifest so the default App Startup initialization is disabled.
Preserve DataBreezeApplication’s runtime.workerFactory configuration, ensuring
WorkManager instantiates SyncWorker through DataBreezeWorkerFactory.

In `@docs/plans/execution-orchestration.json`:
- Around line 18-22: Update docs/plans/execution-orchestration.json lines 18-22
so lastFeaturePullRequest is 13, and update
tools/repo-cli/test/execution-orchestration.test.mjs lines 179-193 to assert
ledger.checkpoint.lastFeaturePullRequest equals 13.

In `@tools/repo-cli/test/android-shell.test.mjs`:
- Around line 43-60: Strengthen the Android manifest assertions in the test by
parsing or structurally matching component opening elements, so an exported
attribute is evaluated only on the relevant component and exported services are
detected regardless of attribute order. Update the backup and extraction rule
checks to require each protected domain on an exclude element, rather than
accepting include elements with matching domain tokens.

---

Nitpick comments:
In `@apps/android/app/build.gradle.kts`:
- Around line 94-98: Replace the Room configuration in the kapt block with KSP
and the Room Gradle plugin for Room 2.7.2, using room {
schemaDirectory("$projectDir/schemas") } and the appropriate plugin aliases.
Move the existing schema file into the plugin-generated variant-specific schema
directory, preserving the current projectDir/schemas root; if kapt is still
required elsewhere, apply libs.plugins.kotlin.kapt rather than a raw plugin ID.

In
`@apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json`:
- Around line 1-79: Regenerate the Room schema JSON for the database containing
SyncQueueEntity using the project’s standard Room schema export process,
ensuring the canonical empty foreignKeys and views arrays are included. Commit
the generated schema output unchanged, preserving the existing identityHash and
database definition.

In `@apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt`:
- Around line 185-192: Update observeQueue in LocalStore to apply
distinctUntilChanged after filtering so collectors emit only when their scoped
queue contents change. Update snapshotQueue to sort matching items by
createdAtEpochMs ascending, then mutationId ascending, matching RoomLocalStore’s
DAO ordering instead of relying on LinkedHashMap insertion order.
- Around line 109-117: Update the Room query and markCompleted method in
LocalStore so the SET clause binds SyncQueueEntity.COMPLETED_STATE directly.
Remove the completedState parameter while preserving the existing accountId,
workspaceId, mutationId filters and return type.

In
`@apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt`:
- Around line 35-51: Update AndroidRuntimeContractTest to import
androidx.work.Data directly and replace the fully qualified Data references with
the imported symbol, while keeping these JVM unit tests in place and continuing
to use Data.Builder.build() without adding Robolectric.

In `@apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt`:
- Around line 54-81: Extend
in_memory_store_cannot_cross_account_or_workspace_boundaries with a third scope
using the same account as first but a different workspace, enqueue its
mutation-1, and assert it remains after clear(first). Apply the same
workspace-dimension coverage to RoomIsolationTest, ensuring the queried
workspace is populated and remains isolated.

In `@tools/repo-cli/test/execution-orchestration.test.mjs`:
- Around line 179-193: Extend the ledger assertions in the execution
orchestration test to verify that ledger.checkpoint.lastFeaturePullRequest
equals 13. Keep the existing task-state and commit validations unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 82d15fdf-1cc0-49ec-9b67-9683ccd2c814

📥 Commits

Reviewing files that changed from the base of the PR and between d26e6be and 4d41549.

📒 Files selected for processing (19)
  • apps/android/README.md
  • apps/android/app/build.gradle.kts
  • apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json
  • apps/android/app/src/androidTest/java/com/databreeze/android/MainActivityTest.kt
  • apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt
  • apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt
  • apps/android/app/src/main/java/com/databreeze/android/DataBreezeApplication.kt
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt
  • apps/android/app/src/main/java/com/databreeze/android/security/DeviceKeyStore.kt
  • apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt
  • apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt
  • apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt
  • apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt
  • apps/android/app/src/test/java/com/databreeze/android/SyncSchedulerContractTest.kt
  • apps/android/gradle/libs.versions.toml
  • docs/operations/foundation-android-2026-08-02.md
  • docs/plans/execution-orchestration.json
  • tools/repo-cli/test/android-shell.test.mjs
  • tools/repo-cli/test/execution-orchestration.test.mjs

Comment thread apps/android/app/src/androidTest/java/com/databreeze/android/MainActivityTest.kt Outdated
Comment thread apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt Outdated
Comment thread apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt Outdated
Comment thread apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt Outdated
Comment on lines +139 to +151
class DataBreezeWorkerFactory(
private val store: LocalStorePort,
private val transport: SyncTransport,
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters,
): ListenableWorker? = when (workerClassName) {
SyncWorker::class.qualifiedName -> SyncWorker(appContext, workerParameters, store, transport)
else -> null
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify custom WorkManager initialization for the Android app.
set -euo pipefail

fd -t f 'DataBreezeApplication.kt' apps/android --exec cat -n {}
fd -t f 'AndroidManifest.xml' apps/android --exec cat -n {}

echo "== Configuration.Provider / workerFactory references =="
rg -nP --type=kt -C 3 'Configuration\.Provider|workerFactory|WorkManager\.initialize'

echo "== Default initializer removal =="
rg -n -C 3 'WorkManagerInitializer|androidx.startup' -g '*.xml'

Repository: DatabreezeService/databreeze-platform

Length of output: 2119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WorkManager and startup declarations =="
rg -n -C 4 'androidx\.work|work-runtime|WorkManagerInitializer|InitializationProvider|androidx\.startup' \
  --glob '!*build*' --glob '!node_modules/**' .

echo "== Android manifests =="
fd -t f -i 'AndroidManifest.xml' apps/android --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}

echo "== Application and runtime factory wiring =="
fd -t f 'DataBreezeApplication.kt' apps/android --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}
rg -n -C 5 'workerFactory|DataBreezeWorkerFactory|WorkManager\.getInstance|Configuration\.Provider|workManagerConfiguration' \
  apps/android --glob '*.kt' --glob '*.java'

Repository: DatabreezeService/databreeze-platform

Length of output: 17093


🌐 Web query:

AndroidX WorkManager Configuration.Provider automatic initialization remove WorkManagerInitializer manifest custom WorkerFactory documentation

💡 Result:

To configure a custom WorkerFactory in WorkManager using Configuration.Provider, you must first disable the default WorkManager automatic initialization [1][2]. 1. Disable Automatic Initialization Since WorkManager 2.6+, it uses the App Startup library internally [1][3]. To disable it, modify your AndroidManifest.xml to remove the WorkManagerInitializer meta-data node from the InitializationProvider [1][3]. Add the following to your AndroidManifest.xml: 2. Implement Configuration.Provider Have your Application class implement the Configuration.Provider interface [1][2]. This allows WorkManager to discover your configuration on-demand when you call WorkManager.getInstance(Context) [1][2]. class MyApplication: Application, Configuration.Provider { override fun getWorkManagerConfiguration: Configuration { return Configuration.Builder.setWorkerFactory(MyCustomWorkerFactory).build } } 3. Important Considerations - Accessing WorkManager: Always use WorkManager.getInstance(Context) to access the singleton instance [1][2]. - Timing: On-demand initialization may delay certain features like automatic rescheduling of work after a crash or device reboot [2][4]. - WorkerFactory: When implementing a custom WorkerFactory, return null if your factory cannot create the requested worker, allowing WorkManager to delegate to its default factory [5]. - Manual Initialization: If you prefer not to use Configuration.Provider, you can manually initialize WorkManager by calling WorkManager.initialize(context, configuration) in your Application.onCreate, but you must still remove the default initializer from the manifest to avoid an IllegalStateException [1][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Declared WorkManager version =="
fd -t f 'libs.versions.toml' apps/android --exec rg -n -C 3 '^work\s*=|androidx-work-runtime' {}

echo "== WorkManager references in dependency configuration =="
fd -t f apps/android --exec rg -n -C 2 'libs\.androidx\.work|androidx-work-runtime|work-runtime-ktx' {} \; 2>/dev/null || true

Repository: DatabreezeService/databreeze-platform

Length of output: 1149


🌐 Web query:

site:developer.android.com/develop/background-work/background-tasks/persistent/configuration/custom-configuration WorkManagerInitializer Configuration.Provider remove

💡 Result:

To remove the default WorkManagerInitializer and provide a custom Configuration, you must update your AndroidManifest.xml file using the tools:node="remove" merge rule [1]. Since WorkManager 2.6, WorkManager uses the App Startup library internally [1]. To disable the default initialization, add the following to your manifest: If you are using other components that rely on App Startup and do not want to remove the entire provider, you can remove only the WorkManagerInitializer node: After removing the default initializer, you should implement the Configuration.Provider interface in your Application class to provide your custom configuration [1]. WorkManager will then call your getWorkManagerConfiguration method automatically when you access WorkManager via WorkManager.getInstance(Context) [1]. Note: For versions of WorkManager older than 2.6, you would remove the workmanager-init provider instead [1].

Citations:


🌐 Web query:

androidx.work WorkManagerInitializer source Configuration.Provider getWorkManagerConfiguration automatic initialization

💡 Result:

WorkManager provides an automatic initialization mechanism suitable for most applications, which ensures the WorkManager singleton is set up before your code runs [1][2]. This is achieved using a built-in ContentProvider and, in versions 2.6.0 and later, the androidx.startup library via the WorkManagerInitializer class [2][3]. If you need to provide a custom configuration (e.g., to customize the Executor or use a WorkerFactory), you must disable this automatic initialization and either manually initialize WorkManager or use the Configuration.Provider interface [2][4][5]. Steps to customize WorkManager initialization: 1. Disable automatic initialization: Update your AndroidManifest.xml to remove the WorkManagerInitializer node [2][4]. 2. Choose one of two initialization methods: Method A: Implement Configuration.Provider (On-demand initialization) Implement the Configuration.Provider interface in your Application class and override getWorkManagerConfiguration to return your custom configuration [4][6][5]. When you access WorkManager using WorkManager.getInstance(context), it will use your provider to initialize itself [4][5]. class MyApplication: Application, Configuration.Provider { override fun getWorkManagerConfiguration: Configuration = Configuration.Builder.setMinimumLoggingLevel(android.util.Log.INFO).build } Method B: Manual Initialization After disabling the default initializer, manually call WorkManager.initialize(context, configuration) within your Application.onCreate or a ContentProvider.onCreate method [2][5]. Note that once initialized, WorkManager cannot be re-initialized with a different configuration; doing so will result in an exception [5].

Citations:


Remove the default WorkManager initializer

DataBreezeApplication already provides runtime.workerFactory. WorkManager 2.10.3 still uses the App Startup initializer unless the manifest removes it. Remove the WorkManagerInitializer metadata so SyncWorker uses DataBreezeWorkerFactory; otherwise sync work cannot be instantiated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt`
around lines 139 - 151, Remove the WorkManagerInitializer metadata from the
application manifest so the default App Startup initialization is disabled.
Preserve DataBreezeApplication’s runtime.workerFactory configuration, ensuring
WorkManager instantiates SyncWorker through DataBreezeWorkerFactory.

Comment thread docs/plans/execution-orchestration.json
Comment thread tools/repo-cli/test/android-shell.test.mjs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt`:
- Around line 91-129: Replace the single shared mutex in both
SharedPreferencesSyncRevocationGuard and InMemorySyncRevocationGuard with
mutexes keyed by AccountWorkspaceScope.stableKey. Ensure withPermit and revoke
for the same scope continue to serialize, while operations and revocations for
different scopes use independent locks and do not block each other.
- Around line 86-89: Extend SyncRevocationGuard with reactivate(scope),
implement it in SharedPreferencesSyncRevocationGuard by clearing the persisted
revoked-${scope.stableKey} flag, and implement equivalent clearing in
InMemorySyncRevocationGuard. Invoke reactivate after successful sign-in and key
initialization, and add coverage for the sign-out/sign-in cycle to confirm
withPermit resumes operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c04fc565-54b0-4518-9253-0bb569ca0017

📥 Commits

Reviewing files that changed from the base of the PR and between 4d41549 and 55b4c66.

📒 Files selected for processing (16)
  • apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json
  • apps/android/app/src/androidTest/java/com/databreeze/android/MainActivityTest.kt
  • apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt
  • apps/android/app/src/main/AndroidManifest.xml
  • apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt
  • apps/android/app/src/main/java/com/databreeze/android/security/DeviceKeyStore.kt
  • apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt
  • apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt
  • apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt
  • apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt
  • apps/android/app/src/test/java/com/databreeze/android/SyncSchedulerContractTest.kt
  • docs/operations/code-review-14-disposition.md
  • docs/plans/execution-orchestration.json
  • tools/repo-cli/test/android-shell.test.mjs
  • tools/repo-cli/test/execution-orchestration.test.mjs
🚧 Files skipped from review as they are similar to previous changes (10)
  • tools/repo-cli/test/execution-orchestration.test.mjs
  • apps/android/app/schemas/com.databreeze.android.storage.DataBreezeDatabase/1.json
  • apps/android/app/src/androidTest/java/com/databreeze/android/RoomIsolationTest.kt
  • apps/android/app/src/androidTest/java/com/databreeze/android/MainActivityTest.kt
  • docs/plans/execution-orchestration.json
  • apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeContractTest.kt
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt
  • tools/repo-cli/test/android-shell.test.mjs
  • apps/android/app/src/test/java/com/databreeze/android/BoundaryTest.kt
  • apps/android/app/src/main/java/com/databreeze/android/storage/LocalStore.kt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt (1)

27-43: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Serialize same-scope sign-in and sign-out.

SyncRevocationGuard serializes individual guard operations, not the complete lifecycle. If signIn pauses after getOrCreate, signOut can revoke the scope, clear local state, and delete the key. signIn can then call reactivate, leaving the scope active without its device key.

Use a per-AccountWorkspaceScope lock around both methods, or expose one atomic lifecycle operation. Add a test for this interleaving.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt`
around lines 27 - 43, Serialize the complete signIn and signOut lifecycles per
AccountWorkspaceScope so they cannot interleave between deviceKeyStore and
revocation or cleanup operations. Add a per-scope lock around both
AndroidRuntime.signIn and AndroidRuntime.signOut, or replace them with an
equivalent atomic lifecycle operation, and add a test covering signIn pausing
before reactivate while signOut runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt`:
- Around line 27-43: Serialize the complete signIn and signOut lifecycles per
AccountWorkspaceScope so they cannot interleave between deviceKeyStore and
revocation or cleanup operations. Add a per-scope lock around both
AndroidRuntime.signIn and AndroidRuntime.signOut, or replace them with an
equivalent atomic lifecycle operation, and add a test covering signIn pausing
before reactivate while signOut runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a353b86-aa9d-40ef-8374-706ec7ebbc5e

📥 Commits

Reviewing files that changed from the base of the PR and between 55b4c66 and 4692492.

📒 Files selected for processing (5)
  • apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt
  • apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt
  • apps/android/app/src/test/java/com/databreeze/android/SyncSchedulerContractTest.kt
  • docs/operations/code-review-14-disposition.md
  • tools/repo-cli/test/android-shell.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/repo-cli/test/android-shell.test.mjs
  • apps/android/app/src/main/java/com/databreeze/android/sync/SyncPorts.kt

fix: serialize Android sign-in and sign-out lifecycle

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeLifecycleTest.kt`:
- Around line 47-49: Make AndroidRuntimeLifecycleTest deterministically contend
on the mutex by starting signOut with CoroutineStart.UNDISPATCHED after
keyStarted.await(), or add equivalent synchronization before releasing key
creation and asserting serialization. Update
docs/operations/code-review-14-disposition.md lines 50-56 to retain the “proves”
claim only once the test establishes contention deterministically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d68378fa-f6de-4f2b-80ab-037f6699a0cd

📥 Commits

Reviewing files that changed from the base of the PR and between 4692492 and 001dc16.

📒 Files selected for processing (3)
  • apps/android/app/src/main/java/com/databreeze/android/AndroidRuntime.kt
  • apps/android/app/src/test/java/com/databreeze/android/AndroidRuntimeLifecycleTest.kt
  • docs/operations/code-review-14-disposition.md

…est-determinism

test: make Android lifecycle race coverage deterministic
@BeforeLights
BeforeLights merged commit a2fcba3 into main Aug 2, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant