Skip to content

fix(android): harden download polling against DB crashes and zombie rows#456

Open
Ayush7614 wants to merge 1 commit into
off-grid-ai:mainfrom
Ayush7614:fix/android-download-zombie-polling
Open

fix(android): harden download polling against DB crashes and zombie rows#456
Ayush7614 wants to merge 1 commit into
off-grid-ai:mainfrom
Ayush7614:fix/android-download-zombie-polling

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 4, 2026

Copy link
Copy Markdown

Summary

  • Wrap startProgressPolling in try/catch so a Room/SQLite migration failure cannot crash the app on launch (documented in docs/PENDING_FIXES.md Feat/local dream #1).
  • Before registering WorkManager observers, synchronously check each active DB row; if WorkManager has no task or only CANCELLED/FAILED work, mark the row FAILED with download_interrupted and emit an error event so ghost downloads show a retry button instead of staying stuck forever (Feat/testing e2e cleanup #2).
  • Extract DownloadPolling.isZombieWorkStates as a pure helper with Android unit tests.
  • Add a JS integration test verifying interrupted downloads hydrate into the download store as failed + retryable.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Related Issues

Relates to #420 (Android download manager reliability)

Test plan

  • npx jest __tests__/integration/models/downloadZombieRecovery.test.ts passes locally
  • cd android && ./gradlew :app:testDebugUnitTest --tests "ai.offgridmobile.download.DownloadManagerModuleTest" (requires Android SDK)
  • Manual: install an app update while a download is running; confirm stuck entries become failed with retry
  • Manual: confirm app does not crash if download DB migration fails on first poll

Summary by CodeRabbit

Summary of Changes

  • Bug Fixes

    • Improved download recovery after restarts by detecting “zombie” background work and restoring affected downloads as failed (with retryable status).
    • Download progress polling now fails more gracefully if errors occur, reducing crash risk.
  • Tests

    • Added unit/integration coverage for zombie download detection, retryability, and hydration of interrupted downloads.
  • Documentation

    • Updated the pending fixes list to mark the completed download-related items as resolved.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c452d6b0-e8ef-4fc8-954b-00ec8ac5c7a2

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc5fa3 and b6a2d1a.

📒 Files selected for processing (5)
  • __tests__/integration/models/downloadZombieRecovery.test.ts
  • android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt
  • android/app/src/main/java/ai/offgridmobile/download/DownloadPolling.kt
  • android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt
  • docs/PENDING_FIXES.md
✅ Files skipped from review due to trivial changes (1)
  • docs/PENDING_FIXES.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • android/app/src/main/java/ai/offgridmobile/download/DownloadPolling.kt
  • tests/integration/models/downloadZombieRecovery.test.ts
  • android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt
  • android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt

📝 Walkthrough

Walkthrough

Adds zombie-download detection to WorkManager polling, marks interrupted downloads failed with DOWNLOAD_INTERRUPTED, and adds unit, integration, and documentation updates around that recovery path.

Changes

Zombie Download Recovery

Layer / File(s) Summary
Zombie state detection helper
android/app/src/main/java/ai/offgridmobile/download/DownloadPolling.kt
New DownloadPolling.isZombieWorkStates classifies a WorkInfo.State list as zombie when empty or all states are CANCELLED/FAILED.
Polling flow updates to detect and handle zombie downloads
android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt
startProgressPolling wraps polling in try/catch, queries WorkInfo per download, detects zombie states, marks the download FAILED with DOWNLOAD_INTERRUPTED in downloadDao, and emits an error event; otherwise registers observers.
Android unit tests for zombie detection and retryability
android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt
Adds WorkInfo import and tests for isZombieWorkStates across multiple state combinations, plus a test verifying DOWNLOAD_INTERRUPTED is retryable with the expected message.
JS hydration integration tests for zombie recovery
__tests__/integration/models/downloadZombieRecovery.test.ts
New suite mocks backgroundDownloadService, resets useDownloadStore, and verifies hydrateDownloadStore() marks interrupted downloads as failed/retryable and retains failed rows with correct downloadIdIndex mapping.
Pending fixes documentation update
docs/PENDING_FIXES.md
Marks the two related download issues as fixed with updated headings.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: hardening Android download polling against DB crashes and zombie rows.
Description check ✅ Passed The description covers the summary, change type, related issue, and test plan, though some template sections are left blank.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Android: prevent polling crash + fail zombie downloads for retry

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Guard download progress polling so Room/SQLite failures don’t crash app startup.
• Reconcile DB “active” downloads with missing/cancelled WorkManager work and fail them as
 interrupted.
• Add Android + JS tests to ensure interrupted downloads hydrate as failed and retryable.
Diagram

graph TD
  RN["RN JS"] --> DMM["DownloadManagerModule"] --> DAO[("Room download DB")] --> ACTIVE["Active rows"] --> WM["WorkManager"] --> STATES["WorkInfo states"] --> Z{"Zombie?"}
  Z -->|"Yes"| FAIL["Set FAILED + emit error"] --> STORE["Download store"]
  Z -->|"No"| OBS["Register observers"] --> STORE

  subgraph Legend
    direction LR
    _svc["Module/Service"] ~~~ _db[("Database")] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist WorkManager UUIDs (or tags) and reconcile via stable identifiers
  • ➕ More precise mapping than reconstructing via unique work name
  • ➕ Enables richer diagnostics (e.g., last known run attempt, timestamps)
  • ➖ Schema/state migration needed for existing rows
  • ➖ More code paths to maintain; still needs handling for pruned work
2. Run a dedicated startup “reconcile” worker instead of doing it in polling
  • ➕ Keeps startProgressPolling focused on observation; reconciliation isolated and retryable
  • ➕ Can run off main startup path, reducing perceived startup risk
  • ➖ Adds another WorkManager job and scheduling complexity
  • ➖ Delays UI correction until reconcile job runs

Recommendation: Current approach is appropriate for the immediate reliability goals: it’s synchronous, local, and fixes UI-stuck downloads deterministically at poll start. Consider persisting stable work identifiers later if mapping ambiguity or additional reconciliation rules become necessary.

Files changed (5) +197 / -6

Bug fix (1) +32 / -4
DownloadManagerModule.ktHarden polling: catch DB failures and fail zombie DB rows +32/-4

Harden polling: catch DB failures and fail zombie DB rows

• Wraps startProgressPolling in try/catch so Room/SQLite issues (e.g., bad migrations) don’t crash the app. Before registering observers, queries WorkManager for each active row and, when work is missing/cancelled/failed, marks the row FAILED with DOWNLOAD_INTERRUPTED and emits an error event; otherwise registers observers as before.

android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt

Refactor (1) +14 / -0
DownloadPolling.ktExtract zombie WorkManager state detection into pure helper +14/-0

Extract zombie WorkManager state detection into pure helper

• Adds a small internal helper object with a pure function to detect “zombie” work (no work infos or only CANCELLED/FAILED states), enabling unit testing without Room/WorkManager integration.

android/app/src/main/java/ai/offgridmobile/download/DownloadPolling.kt

Tests (2) +147 / -0
downloadZombieRecovery.test.tsAdd JS integration coverage for interrupted/zombie download hydration +76/-0

Add JS integration coverage for interrupted/zombie download hydration

• Introduces integration tests that mock the background download service and assert interrupted downloads hydrate into the store as FAILED with a retryable error code. Also verifies failed rows are retained/indexed so the UI can present a retry action.

tests/integration/models/downloadZombieRecovery.test.ts

DownloadManagerModuleTest.ktAdd unit tests for zombie detection and retryable interrupted reason +71/-0

Add unit tests for zombie detection and retryable interrupted reason

• Adds Android unit tests for DownloadPolling.isZombieWorkStates across empty, cancelled, failed, and active states. Also asserts DOWNLOAD_INTERRUPTED is retryable and has the expected user-facing message.

android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt

Documentation (1) +4 / -2
PENDING_FIXES.mdMark polling crash + zombie download issues as fixed +4/-2

Mark polling crash + zombie download issues as fixed

• Updates the pending fixes document to reflect that the previously documented startProgressPolling SQLite crash and zombie download entries are addressed in this branch.

docs/PENDING_FIXES.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 25 rules

Grey Divider


Remediation recommended

1. Cancellation swallowed 🐞 Bug ☼ Reliability
Description
startProgressPolling() catches Exception, which includes CancellationException, so coroutine
cancellation won’t propagate normally when the module scope is cancelled (e.g., teardown). This can
lead to inconsistent lifecycle behavior (e.g., partially running polling work) instead of clean
cancellation.
Code

android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[R315-348]

+            try {
+                val active = withContext(Dispatchers.IO) {
+                    downloadDao.getAllDownloads().first().filter {
+                        it.status == DownloadStatus.QUEUED || it.status == DownloadStatus.RUNNING
+                    }
+                }
+                for (download in active) {
+                    if (workObservers.containsKey(download.id)) continue
+                    val workInfos = withContext(Dispatchers.IO) {
+                        workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
+                    }
+                    if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
+                        withContext(Dispatchers.IO) {
+                            downloadDao.updateStatus(
+                                download.id,
+                                DownloadStatus.FAILED,
+                                DownloadReason.DOWNLOAD_INTERRUPTED,
+                            )
+                        }
+                        DownloadEventBridge.error(
+                            download.id,
+                            download.fileName,
+                            download.modelId,
+                            DownloadReason.messageFor(DownloadReason.DOWNLOAD_INTERRUPTED)
+                                ?: "Download was interrupted.",
+                            DownloadReason.DOWNLOAD_INTERRUPTED,
+                        )
+                    } else {
+                        registerObserver(download.id, download.fileName, download.modelId)
+                    }
                }
+            } catch (_: Exception) {
+                // DB unavailable — polling skipped, no crash
            }
-            active.forEach { if (!workObservers.containsKey(it.id)) registerObserver(it.id, it.fileName, it.modelId) }
Relevance

⭐⭐⭐ High

Team previously rejected overly-broad catches; likely accept rethrowing CancellationException to
preserve teardown semantics.

PR-#198
PR-#111

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The module uses a cancellable CoroutineScope that is explicitly cancelled on teardown; however, the
new polling code catches all Exceptions (including CancellationException) and does not rethrow, so
cancellation is treated like a normal error and swallowed.

android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[37-54]
android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`startProgressPolling()` wraps all polling logic in `try { ... } catch (_: Exception) {}`. In Kotlin coroutines, `CancellationException` is an `Exception`, so this catch will intercept cancellations and prevent normal cancellation propagation semantics.

### Issue Context
The module cancels its `scope` during teardown (`onCatalystInstanceDestroy()`), so `startProgressPolling()` should not swallow cancellation.

### Fix Focus Areas
- android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

### Suggested fix
- Change the catch to capture the exception and rethrow cancellation:
 - `catch (e: Exception) { if (e is CancellationException) throw e; ... }`
- (Optional but helpful) Narrow the catch to known DB exceptions if that’s the only intended suppression (e.g., SQLiteException).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Polling aborts on error 🐞 Bug ☼ Reliability
Description
A single exception while processing one active row (e.g., during WorkManager work info lookup) exits
the entire polling loop, so later active downloads won’t be zombie-checked or have observers
registered in that cycle. The empty catch also makes these failures silent, reducing debuggability
when polling stops working.
Code

android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[R315-348]

+            try {
+                val active = withContext(Dispatchers.IO) {
+                    downloadDao.getAllDownloads().first().filter {
+                        it.status == DownloadStatus.QUEUED || it.status == DownloadStatus.RUNNING
+                    }
+                }
+                for (download in active) {
+                    if (workObservers.containsKey(download.id)) continue
+                    val workInfos = withContext(Dispatchers.IO) {
+                        workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
+                    }
+                    if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
+                        withContext(Dispatchers.IO) {
+                            downloadDao.updateStatus(
+                                download.id,
+                                DownloadStatus.FAILED,
+                                DownloadReason.DOWNLOAD_INTERRUPTED,
+                            )
+                        }
+                        DownloadEventBridge.error(
+                            download.id,
+                            download.fileName,
+                            download.modelId,
+                            DownloadReason.messageFor(DownloadReason.DOWNLOAD_INTERRUPTED)
+                                ?: "Download was interrupted.",
+                            DownloadReason.DOWNLOAD_INTERRUPTED,
+                        )
+                    } else {
+                        registerObserver(download.id, download.fileName, download.modelId)
+                    }
                }
+            } catch (_: Exception) {
+                // DB unavailable — polling skipped, no crash
            }
-            active.forEach { if (!workObservers.containsKey(it.id)) registerObserver(it.id, it.fileName, it.modelId) }
Relevance

⭐⭐ Medium

No clear prior guidance on per-item try/catch vs whole-loop; team sometimes prefers coarse catch to
avoid crashes.

PR-#280

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code encloses the full polling loop in a single try/catch; any exception thrown during a single
iteration (including the blocking WorkManager query) will break out to the catch and stop further
processing, and the catch currently logs nothing.

android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`startProgressPolling()` wraps the entire “active downloads” loop in one try/catch. If any one download iteration throws (DB read, WorkManager query, event emission), the coroutine jumps to the catch and skips processing the remaining active downloads.

### Issue Context
This PR’s goal is to *harden* polling and zombie recovery; the current structure can still leave some downloads stuck/unobserved if one problematic row/work query fails.

### Fix Focus Areas
- android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

### Suggested fix
- Keep a guarded try/catch around the initial DB read (if desired), but move exception handling inside the per-download loop so one failure doesn’t prevent processing others.
- Add a warning log when skipping due to exception (at least include downloadId/modelId) while still preventing crashes.
- Ensure cancellation still propagates (rethrow `CancellationException`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +315 to 348
try {
val active = withContext(Dispatchers.IO) {
downloadDao.getAllDownloads().first().filter {
it.status == DownloadStatus.QUEUED || it.status == DownloadStatus.RUNNING
}
}
for (download in active) {
if (workObservers.containsKey(download.id)) continue
val workInfos = withContext(Dispatchers.IO) {
workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
}
if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
withContext(Dispatchers.IO) {
downloadDao.updateStatus(
download.id,
DownloadStatus.FAILED,
DownloadReason.DOWNLOAD_INTERRUPTED,
)
}
DownloadEventBridge.error(
download.id,
download.fileName,
download.modelId,
DownloadReason.messageFor(DownloadReason.DOWNLOAD_INTERRUPTED)
?: "Download was interrupted.",
DownloadReason.DOWNLOAD_INTERRUPTED,
)
} else {
registerObserver(download.id, download.fileName, download.modelId)
}
}
} catch (_: Exception) {
// DB unavailable — polling skipped, no crash
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Cancellation swallowed 🐞 Bug ☼ Reliability

startProgressPolling() catches Exception, which includes CancellationException, so coroutine
cancellation won’t propagate normally when the module scope is cancelled (e.g., teardown). This can
lead to inconsistent lifecycle behavior (e.g., partially running polling work) instead of clean
cancellation.
Agent Prompt
### Issue description
`startProgressPolling()` wraps all polling logic in `try { ... } catch (_: Exception) {}`. In Kotlin coroutines, `CancellationException` is an `Exception`, so this catch will intercept cancellations and prevent normal cancellation propagation semantics.

### Issue Context
The module cancels its `scope` during teardown (`onCatalystInstanceDestroy()`), so `startProgressPolling()` should not swallow cancellation.

### Fix Focus Areas
- android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

### Suggested fix
- Change the catch to capture the exception and rethrow cancellation:
  - `catch (e: Exception) { if (e is CancellationException) throw e; ... }`
- (Optional but helpful) Narrow the catch to known DB exceptions if that’s the only intended suppression (e.g., SQLiteException).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +315 to 348
try {
val active = withContext(Dispatchers.IO) {
downloadDao.getAllDownloads().first().filter {
it.status == DownloadStatus.QUEUED || it.status == DownloadStatus.RUNNING
}
}
for (download in active) {
if (workObservers.containsKey(download.id)) continue
val workInfos = withContext(Dispatchers.IO) {
workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
}
if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
withContext(Dispatchers.IO) {
downloadDao.updateStatus(
download.id,
DownloadStatus.FAILED,
DownloadReason.DOWNLOAD_INTERRUPTED,
)
}
DownloadEventBridge.error(
download.id,
download.fileName,
download.modelId,
DownloadReason.messageFor(DownloadReason.DOWNLOAD_INTERRUPTED)
?: "Download was interrupted.",
DownloadReason.DOWNLOAD_INTERRUPTED,
)
} else {
registerObserver(download.id, download.fileName, download.modelId)
}
}
} catch (_: Exception) {
// DB unavailable — polling skipped, no crash
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Polling aborts on error 🐞 Bug ☼ Reliability

A single exception while processing one active row (e.g., during WorkManager work info lookup) exits
the entire polling loop, so later active downloads won’t be zombie-checked or have observers
registered in that cycle. The empty catch also makes these failures silent, reducing debuggability
when polling stops working.
Agent Prompt
### Issue description
`startProgressPolling()` wraps the entire “active downloads” loop in one try/catch. If any one download iteration throws (DB read, WorkManager query, event emission), the coroutine jumps to the catch and skips processing the remaining active downloads.

### Issue Context
This PR’s goal is to *harden* polling and zombie recovery; the current structure can still leave some downloads stuck/unobserved if one problematic row/work query fails.

### Fix Focus Areas
- android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt[312-349]

### Suggested fix
- Keep a guarded try/catch around the initial DB read (if desired), but move exception handling inside the per-download loop so one failure doesn’t prevent processing others.
- Add a warning log when skipping due to exception (at least include downloadId/modelId) while still preventing crashes.
- Ensure cancellation still propagates (rethrow `CancellationException`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@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.

🧹 Nitpick comments (2)
android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt (1)

310-368: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering a SUCCEEDED state case.

Good coverage of terminal-failure/empty combinations vs. active states. Optionally add a case with WorkInfo.State.SUCCEEDED alone to lock in that a completed-but-untracked work item isn't misclassified as zombie by the helper.

🤖 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
`@android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt`
around lines 310 - 368, Add a test case in DownloadManagerModuleTest for
DownloadPolling.isZombieWorkStates with WorkInfo.State.SUCCEEDED alone to cover
the completed-work path. Keep it alongside the existing isZombieWorkStates tests
and assert that SUCCEEDED is not treated as zombie, matching the current
active-state rejection cases.
android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt (1)

315-348: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Move exception handling inside the loop so one bad download doesn't skip the rest.

The try/catch wraps the entire loop. If getWorkInfosForUniqueWork(...).get(), updateStatus, or registerObserver throws for one download, the loop aborts and every subsequent active download is silently skipped — those rows never get an observer registered and won't update in the UI until the next poll. The blanket catch (_: Exception) also swallows the failure with no log, making such partial failures invisible.

Consider handling per-download and logging unexpected failures:

♻️ Suggested per-item isolation
             try {
                 val active = withContext(Dispatchers.IO) {
                     downloadDao.getAllDownloads().first().filter {
                         it.status == DownloadStatus.QUEUED || it.status == DownloadStatus.RUNNING
                     }
                 }
                 for (download in active) {
                     if (workObservers.containsKey(download.id)) continue
-                    val workInfos = withContext(Dispatchers.IO) {
-                        workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
-                    }
-                    if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
-                        ...
-                    } else {
-                        registerObserver(download.id, download.fileName, download.modelId)
-                    }
+                    try {
+                        val workInfos = withContext(Dispatchers.IO) {
+                            workManager.getWorkInfosForUniqueWork(WorkerDownload.workName(download.id)).get()
+                        }
+                        if (DownloadPolling.isZombieWorkStates(workInfos.map { it.state })) {
+                            // ... mark FAILED + emit error ...
+                        } else {
+                            registerObserver(download.id, download.fileName, download.modelId)
+                        }
+                    } catch (e: Exception) {
+                        Log.w(NAME, "Polling failed for ${download.id}", e)
+                    }
                 }
-            } catch (_: Exception) {
-                // DB unavailable — polling skipped, no crash
+            } catch (e: Exception) {
+                Log.w(NAME, "Progress polling skipped: DB unavailable", e)
             }
🤖 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 `@android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt`
around lines 315 - 348, The polling block in DownloadManagerModule’s
active-download loop is too broadly wrapped, so one exception from
getWorkInfosForUniqueWork, updateStatus, or registerObserver can stop processing
the remaining downloads. Move the exception handling inside the for loop so each
download is isolated, and keep the outer DB-unavailable guard only around the
initial fetch. Also add logging in the per-download catch so failures are
visible instead of being silently swallowed.
🤖 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.

Nitpick comments:
In
`@android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt`:
- Around line 315-348: The polling block in DownloadManagerModule’s
active-download loop is too broadly wrapped, so one exception from
getWorkInfosForUniqueWork, updateStatus, or registerObserver can stop processing
the remaining downloads. Move the exception handling inside the for loop so each
download is isolated, and keep the outer DB-unavailable guard only around the
initial fetch. Also add logging in the per-download catch so failures are
visible instead of being silently swallowed.

In
`@android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt`:
- Around line 310-368: Add a test case in DownloadManagerModuleTest for
DownloadPolling.isZombieWorkStates with WorkInfo.State.SUCCEEDED alone to cover
the completed-work path. Keep it alongside the existing isZombieWorkStates tests
and assert that SUCCEEDED is not treated as zombie, matching the current
active-state rejection cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73797697-31ce-421f-bd89-bb5e179423a0

📥 Commits

Reviewing files that changed from the base of the PR and between a0535cd and 7fc5fa3.

📒 Files selected for processing (5)
  • __tests__/integration/models/downloadZombieRecovery.test.ts
  • android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt
  • android/app/src/main/java/ai/offgridmobile/download/DownloadPolling.kt
  • android/app/src/test/java/ai/offgridmobile/download/DownloadManagerModuleTest.kt
  • docs/PENDING_FIXES.md

Wrap startProgressPolling in try/catch so a Room/SQLite failure cannot
crash the app, and reconcile QUEUED/RUNNING DB rows whose WorkManager
tasks were cancelled or dropped after an app update. Mark zombies FAILED
with download_interrupted so the Download Manager shows a retry button.
@Ayush7614 Ayush7614 force-pushed the fix/android-download-zombie-polling branch from 7fc5fa3 to b6a2d1a Compare July 4, 2026 09:27
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

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