Skip to content

Code review fixes: DB integrity, concurrency, security, perf + tests - #27

Merged
anod merged 4 commits into
mainfrom
feature/code-review-fixes
Aug 1, 2026
Merged

Code review fixes: DB integrity, concurrency, security, perf + tests#27
anod merged 4 commits into
mainfrom
feature/code-review-fixes

Conversation

@anod

@anod anod commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

A code-review pass over the app focused on bugs, security, performance, and simplifications, with unit tests added for the fixes.

Bug fixes

  • DB write path (ShortcutsDatabase) — replace the INSERTUPDATE fallback with delete-then-insert so last_insert_rowid() is always valid. Previously, overwriting an occupied (targetId, position) slot attached folder children to a stale parent id, and copyShortcut threw an uncaught SQLiteConstraintException onto an occupied slot. FK ON DELETE CASCADE now cleans up the replaced row's folder items.
  • Corrupt rows — shortcuts with empty/unparseable intents map to ID_UNKNOWN so existing isValid gates suppress them; observeFolder now filters invalid items to match loadFolderItems.
  • ModeDetector concurrency — guard shared pref/event state with the existing lock across onRegister/forceState/switchOn/switchOff and the onBroadcastReceive state block; prefState 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 (scripts/switch_incar.sh).

Performance

  • ModeService — stop calling runBlocking on 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.
  • BitmapLruCachesizeOf() no longer rounds sub-kilobyte bitmaps to 0; cache budget measured in kilobytes to match.
  • BackupManager — replace newSingleThreadContext (leaks a thread) with Dispatchers.IO.

Cleanup

  • Remove dead AcceptCallActivity (never launched; superseded by ModePhoneStateListener.acceptRingingCall()).

Tests

  • content: BitmapLruCacheUnitTest, ShortcutCorruptRowUnitTest, ShortcutOverwriteUnitTest.
  • app: bootstrap local unit-test infrastructure (JUnit4 catalog entry + Robolectric) and add ModeDetectorTest and ExportedActivitiesTest.

Verification

  • ./gradlew :content:testAndroidHostTest :app:testDebugUnitTest26 tests, 0 failures.
  • :app:compileDebugKotlin clean.

Notes

  • BrowserUrlSender intentionally left unchanged (only triggered by a user click).

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
Copilot AI review requested due to automatic review settings August 1, 2026 08:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 using Dispatchers.IO for 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
Copilot AI review requested due to automatic review settings August 1, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
Copilot AI review requested due to automatic review settings August 1, 2026 11:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
Copilot AI review requested due to automatic review settings August 1, 2026 11:39
@anod
anod merged commit 1fb5e25 into main Aug 1, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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",

Comment on lines 282 to 294
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)
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.

2 participants