Code review fixes: DB integrity, concurrency, security, perf + tests - #27
Conversation
Bug fixes - ShortcutsDatabase write path: replace INSERT-then-UPDATE fallback with delete-then-insert so last_insert_rowid() is always valid. Folder children were being attached to a stale parent id when overwriting an occupied (targetId, position) slot, and copyShortcut threw an uncaught SQLiteConstraintException onto an occupied slot. FK ON DELETE CASCADE now cleans up the replaced row's folder items. - Suppress corrupt shortcut rows (empty / unparseable intent) by mapping them to ID_UNKNOWN so isValid gates hide them; observeFolder now filters invalid items to match loadFolderItems. - ModeDetector: guard shared pref/event state with the existing lock across onRegister/forceState/switchOn/switchOff and the onBroadcastReceive state block; prefState getter returns a defensive copy. Security - Set exported=false on OverlayActivity, ShortcutActivity and SwitchInCarActivity (launched only via internal PendingIntents). Add a debug-only manifest overlay re-exporting SwitchInCarActivity for the adb switch script. Performance - ModeService: stop calling runBlocking on the main thread. Start foreground immediately with a lightweight notification, then build the rich one off the main thread and swap it in. - BitmapLruCache: sizeOf() no longer rounds sub-kilobyte bitmaps to 0; cache budget measured in kilobytes to match. - BackupManager: replace newSingleThreadContext with Dispatchers.IO. Cleanup - Remove dead AcceptCallActivity (never launched; superseded by ModePhoneStateListener.acceptRingingCall()). Tests - content: BitmapLruCacheUnitTest, ShortcutCorruptRowUnitTest, ShortcutOverwriteUnitTest. - app: bootstrap local unit-test infra (junit4 catalog entry + Robolectric) and add ModeDetectorTest and ExportedActivitiesTest. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 698d9ed2-0a45-4e67-9777-549537a61c98
There was a problem hiding this comment.
Pull request overview
This PR tightens correctness and safety in the app’s in-car mode + shortcuts database flows, hardens component exposure in the manifest, and adds Robolectric-based unit tests to prevent regressions.
Changes:
- Fixes shortcut DB overwrite/copy behavior and invalid-intent row handling; aligns folder observation with existing validity gating.
- Improves performance/safety by avoiding main-thread blocking in
ModeService, correcting bitmap cache sizing, and usingDispatchers.IOfor backup/restore. - Adds local unit-test infrastructure (JUnit4 + Robolectric) and regression tests for DB integrity, mode-detector concurrency, and exported-activity security.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| gradle/libs.versions.toml | Adds a JUnit4 catalog entry to support local unit tests. |
| content/src/androidMain/kotlin/info/anodsplace/carwidget/content/db/ShortcutsDatabase.kt | DB integrity fixes (delete-then-insert, corrupt row suppression) and folder observation filtering. |
| content/src/androidMain/kotlin/info/anodsplace/carwidget/content/BitmapLruCache.kt | Fixes LruCache sizing so sub-KB bitmaps count and cache budget matches units. |
| content/src/androidMain/kotlin/info/anodsplace/carwidget/content/backup/BackupManager.kt | Replaces per-call single-thread contexts with Dispatchers.IO. |
| content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutOverwriteUnitTest.kt | Regression tests for overwrite + copy behavior with occupied slots and FK cascades. |
| content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutCorruptRowUnitTest.kt | Regression tests ensuring corrupt intent rows are suppressed/filtered consistently. |
| content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/BitmapLruCacheUnitTest.kt | Tests guarding bitmap cache sizing/budget behavior. |
| app/src/test/kotlin/com/anod/car/home/incar/ModeDetectorTest.kt | Tests for ModeDetector state isolation and flag mapping. |
| app/src/test/kotlin/com/anod/car/home/ExportedActivitiesTest.kt | Security regression guard for android:exported expectations (debug vs release behavior). |
| app/src/main/java/com/anod/car/home/notifications/InCarModeNotificationFactory.kt | Splits a basic vs rich notification to support fast foreground entry. |
| app/src/main/java/com/anod/car/home/incar/ModeService.kt | Removes runBlocking on main; enters foreground immediately then updates notification asynchronously. |
| app/src/main/java/com/anod/car/home/incar/ModeDetector.kt | Synchronization hardening and defensive copy for shared pref state. |
| app/src/main/java/com/anod/car/home/incar/AcceptCallActivity.kt | Removes dead activity implementation. |
| app/src/main/AndroidManifest.xml | Sets exported=false for internal activities and removes AcceptCallActivity from manifest. |
| app/src/debug/AndroidManifest.xml | Debug-only overlay re-exporting SwitchInCarActivity for adb script usage. |
| app/build.gradle.kts | Adds Robolectric/JUnit4 test deps and unit test options for Android resources. |
Address PR review: copyShortcut deleted the destination slot before duplicating, which for a self-copy (destination already holds sourceShortcutId) dropped the source row and made duplicateShortcut a silent no-op. Early-return when the slot already holds the source. Also flowOn(Dispatchers.IO) for observeFolder so the isValid filter runs off the UI thread. Adds self-copy regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf666aec-f66e-4aa1-b001-2f43c2ed883d
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
app/src/main/java/com/anod/car/home/incar/ModeService.kt:118
- The rich notification build is dispatched to Dispatchers.Default, but the build path performs blocking work (e.g., PackageManager icon loads in ShortcutIconLoader.Activity.load). Use Dispatchers.IO here to avoid starving the CPU-bound Default dispatcher with blocking calls.
val notification = withContext(Dispatchers.Default) { notificationFactory.create() }
Address PR review: rich notification build performs blocking work (PackageManager.getActivityIcon via ShortcutIconLoader), so run it on Dispatchers.IO rather than the CPU-bound Default pool to avoid starving it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf666aec-f66e-4aa1-b001-2f43c2ed883d
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
app/src/main/java/com/anod/car/home/incar/ModeService.kt:125
- updateNotification() catches Exception and will also swallow kotlinx.coroutines.CancellationException, preventing cooperative cancellation when the service is destroyed. Re-throw CancellationException and only log other exceptions.
} catch (e: Exception) {
AppLog.e(e)
}
app/src/test/kotlin/com/anod/car/home/ExportedActivitiesTest.kt:42
- This test asserts SwitchInCarActivity is exported=true unconditionally, but release builds intentionally keep exported=false. If this test ever runs under a non-debug unit test variant (e.g., testReleaseUnitTest), it will fail. Assert against BuildConfig.DEBUG so it validates both variants.
fun switchInCarActivityIsReExportedInDebugForSwitchScript() {
val exported = exportedByActivityName()
assertEquals(true, exported["com.anod.car.home.incar.SwitchInCarActivity"])
}
…build-variant aware Address PR review: ModeService.updateNotification() now re-throws CancellationException so the service coroutine can still be cancelled cooperatively; only other exceptions are logged. ExportedActivitiesTest asserts SwitchInCarActivity's exported flag against BuildConfig.DEBUG instead of unconditionally true, so it stays correct under a release unit-test variant (release keeps it exported=false). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf666aec-f66e-4aa1-b001-2f43c2ed883d
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutOverwriteUnitTest.kt:36
- This test uses a fixed on-disk database name. Because the file can persist across test methods within the same JVM, it can leak state between tests and make them order-dependent/flaky. Prefer an in-memory DB (name = null) or a unique per-test name.
name = "shortcut-overwrite-unit.db",
content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutCorruptRowUnitTest.kt:34
- This test uses a fixed on-disk database name. Because the file can persist across test methods within the same JVM, it can leak state between tests and make them order-dependent/flaky. Prefer an in-memory DB (name = null) or a unique per-test name.
name = "shortcut-corrupt-unit.db",
| if (sourceShortcutId != Shortcut.ID_UNKNOWN) { | ||
| // Copying a shortcut onto the slot it already occupies is a no-op. Skip the | ||
| // delete-then-insert below, which would otherwise drop the source row and turn | ||
| // duplicateShortcut (which copies FROM that same row) into a silent failure. | ||
| val occupantId = db.shortcutsQueries.selectTargetPosition(targetId, position) | ||
| .executeAsOneOrNull()?.shortcutId | ||
| if (occupantId == sourceShortcutId) { | ||
| return@transactionWithResult true | ||
| } | ||
| // Free the destination slot first: duplicateShortcut does an INSERT that would | ||
| // otherwise violate UNIQUE(targetId, position) and throw when the slot is taken. | ||
| db.shortcutsQueries.deleteTargetPosition(targetId, position) | ||
| val result = db.shortcutsQueries.duplicateShortcut(targetId, position, sourceShortcutId) |
Summary
A code-review pass over the app focused on bugs, security, performance, and simplifications, with unit tests added for the fixes.
Bug fixes
ShortcutsDatabase) — replace theINSERT→UPDATEfallback with delete-then-insert solast_insert_rowid()is always valid. Previously, overwriting an occupied(targetId, position)slot attached folder children to a stale parent id, andcopyShortcutthrew an uncaughtSQLiteConstraintExceptiononto an occupied slot. FKON DELETE CASCADEnow cleans up the replaced row's folder items.ID_UNKNOWNso existingisValidgates suppress them;observeFoldernow filters invalid items to matchloadFolderItems.ModeDetectorconcurrency — guard shared pref/event state with the existing lock acrossonRegister/forceState/switchOn/switchOffand theonBroadcastReceivestate block;prefStatereturns a defensive copy.Security
exported=falseonOverlayActivity,ShortcutActivity, andSwitchInCarActivity(launched only via internalPendingIntents). Add a debug-only manifest overlay re-exportingSwitchInCarActivityfor the adb switch script (scripts/switch_incar.sh).Performance
ModeService— stop callingrunBlockingon the main thread (ANR risk). Start foreground immediately with a lightweight notification, then build the rich one off the main thread and swap it in.BitmapLruCache—sizeOf()no longer rounds sub-kilobyte bitmaps to0; cache budget measured in kilobytes to match.BackupManager— replacenewSingleThreadContext(leaks a thread) withDispatchers.IO.Cleanup
AcceptCallActivity(never launched; superseded byModePhoneStateListener.acceptRingingCall()).Tests
BitmapLruCacheUnitTest,ShortcutCorruptRowUnitTest,ShortcutOverwriteUnitTest.ModeDetectorTestandExportedActivitiesTest.Verification
./gradlew :content:testAndroidHostTest :app:testDebugUnitTest→ 26 tests, 0 failures.:app:compileDebugKotlinclean.Notes
BrowserUrlSenderintentionally left unchanged (only triggered by a user click).