Skip to content

feat: APK Inspect feature + faithful pending-row UX#489

Merged
rainxchzed merged 11 commits into
mainfrom
feat/apk-inspect-and-pending-row-polish
May 3, 2026
Merged

feat: APK Inspect feature + faithful pending-row UX#489
rainxchzed merged 11 commits into
mainfrom
feat/apk-inspect-and-pending-row-polish

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 3, 2026

Combines two related changes that share the apps-list / pending-install surface:

A — Faithful pending row (commit 1)

When the user taps Install but cancels the system installer prompt, the row used to land in apps with a GitHub Store logo and a broken "Open" button (snackbar of failure). Now:

  • SaveInstalledAppParams carries pendingInstallFilePath. DetailsViewModel.saveInstalledAppToDatabase plumbs the parked file path through whenever the install came back as DELEGATED_TO_SYSTEM, so the freshly-created DB row actually points at its parked APK.
  • InstalledAppIcon (Android) gains an apkFilePath fallback. When the package isn't on the system yet it loads the icon out of the parked APK via PackageManager.getPackageArchiveInfo (sourceDir patched so loadIcon() returns the real icon, not the platform default). AppItemCard and CompactAppRow pass the parked path.
  • Apps row CTA is now state-aware:
    • pendingInstallFilePath != null → primary "Install" button + a small Discard escape hatch.
    • isPendingInstall && pendingInstallFilePath == null (legacy/edge) → "Discard" replaces the broken Open.
    • Compact rows: overflow swaps "Uninstall" for "Discard" when pending.
  • New OnDiscardPendingInstall action: cancels orchestrator entry, deletes parked APK file, removes DB row.

B — APK Inspect feature (commit 2)

A "peek inside before installing" bottom sheet that works for both parked APKs and installed packages.

  • core/domain adds ApkInspector + ApkInspection + ApkPermission + ProtectionLevel (NORMAL / DANGEROUS / SIGNATURE / PRIVILEGED / UNKNOWN).
  • core/data/androidMain ships AndroidApkInspector reading from PackageManager with GET_PERMISSIONS | GET_ACTIVITIES | GET_SERVICES | GET_RECEIVERS | GET_SIGNING_CERTIFICATES. Signing fingerprint extraction lifted into a shared SigningFingerprint helper. JVM ships a stub that returns null.
  • ApkInspectSheet (Material 3 ModalBottomSheet) renders identity, compatibility, permissions (color-coded by danger), components and file info. Dangerous permissions float to the top of the list.
  • DetailsViewModel resolves the source automatically — installed manifest wins over a parked file when the row isn't pending; falls back the other way for pre-install inspections.
  • New InspectApkButton next to SmartInstallButton on the details screen. The first time it appears for a user, it does a slow scale-pulse + tilt and pops a tooltip-style coachmark with arrow ("Peek inside before installing — permissions, signing, what apps can do"). Acknowledged on tap or when the inspect sheet opens, persisted via TweaksRepository.apkInspectCoachmarkShown.

Test plan

  • Tap Install, then cancel the system installer prompt: app appears in the apps list with its real icon (not GitHub Store logo). Row shows "Install" CTA + Discard. Discard removes the row and parked file.
  • First time the inspect button appears next to Install on the Details screen, the pulse + coachmark fire. Tap "Got it" or open the sheet → coachmark never returns on subsequent screens.
  • Inspect sheet on a parked APK: shows source = "Parked file", real icon, declared permissions sorted with red dangerous-bucket on top. Sheet renders even before install.
  • Inspect sheet on an installed package: shows source = "Installed package", same data, plus runtime grant state on dangerous permissions.
  • Permissions list on a debuggable APK (e.g. a dev build) shows the "Debuggable build" warning chip in identity.
  • Apps list overflow on a pending row (no parked file) shows "Discard" instead of "Uninstall".

Summary by CodeRabbit

  • New Features

    • APK Inspect UI: bottom sheet, Inspect button with coachmark, shows permissions, components, signing, SDK targets, size, source, and icon fallback from parked APKs.
    • Platform APK inspection implemented on Android (desktop is a noop).
    • Discard pending-install flow with confirmation dialog across app lists and item rows.
  • Bug Fixes

    • Clear stale parked APK metadata and delete parked files after installs or during sync.
  • Documentation

    • Localized strings for APK Inspect UI and discard dialog.
  • Settings

    • One-shot APK Inspect coachmark persisted.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7dfcc1a-a852-49c7-b8b2-f458668b265f

📥 Commits

Reviewing files that changed from the base of the PR and between 9de8a0f and e0b8909.

📒 Files selected for processing (3)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
✅ Files skipped from review due to trivial changes (1)
  • core/presentation/src/commonMain/composeResources/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt

Walkthrough

Adds an APK inspection feature and UI (button, coachmark, bottom sheet), domain model and ApkInspector interface, Android/JVM inspector implementations, DI wiring, parked-APK tracking and discard flows, icon fallback from parked APKs, preferences/repo changes, and sync/cleanup logic for stale parked files.

Changes

APK Inspection + Pending-Install UX

Layer / File(s) Summary
Data Shape / Domain
core/domain/src/commonMain/.../ApkInspector.kt, core/domain/src/commonMain/.../ApkInspection.kt
Adds ApkInspector interface and domain types ApkInspection, ApkPermission, and ProtectionLevel.
Repository / Preferences
core/domain/.../TweaksRepository.kt, core/data/.../TweaksRepositoryImpl.kt
Adds coachmark preference API and boolean preference key apk_inspect_coachmark_shown.
Platform Implementations
core/data/src/androidMain/.../AndroidApkInspector.kt, core/data/src/androidMain/.../SigningFingerprint.kt, core/data/src/jvmMain/.../DesktopApkInspector.kt
Implements Android APK inspection (file and installed package) with PackageManager compatibility helpers and signing-fingerprint extraction; adds desktop no-op inspector returning null.
DI Wiring
core/data/.../PlatformModule.android.kt, core/data/.../PlatformModule.jvm.kt, composeApp/.../ViewModelsModule.kt
Registers ApkInspector singletons for Android and JVM; DetailsViewModel factory now injects apkInspector = get().
ViewModel / Business Logic
feature/details/presentation/.../DetailsViewModel.kt, feature/details/presentation/.../DetailsState.kt, feature/details/presentation/.../DetailsAction.kt
Injects ApkInspector; adds actions OnInspectApk, OnDismissApkInspect, OnAcknowledgeApkInspectCoachmark; manages sheet loading/visibility/state; resolves inspection from installed package or parked APK path; persists parked path for pending installs; observes coachmark flag.
UI Components
feature/details/presentation/.../ApkInspectSheet.kt, .../InspectApkButton.kt, .../sections/Header.kt
New ApkInspectSheet bottom sheet (loading/empty/inspection sections), InspectApkButton with animated coachmark popup, and header wiring to show the button conditionally.
App List & Pending-Install Flows
feature/apps/presentation/.../AppsAction.kt, .../AppsRoot.kt, .../AppsViewModel.kt, .../CompactAppRow.kt
Adds discard-pending-install actions and UI; threads onDiscardPendingClick into cards/overflow; AppsViewModel.discardPendingInstall() cancels orchestrator, deletes parked APK, and removes DB row.
Icon Fallback
feature/apps/presentation/.../InstalledAppIcon.kt, .../InstalledAppIcon.android.kt, .../InstalledAppIcon.jvm.kt
Extends InstalledAppIcon API with optional apkFilePath; Android implementation attempts to load icon from parked APK when system icon lookup fails; JVM signature updated to accept param.
Installation Persistence
feature/details/domain/.../SaveInstalledAppParams.kt, feature/details/data/.../InstallationManagerImpl.kt
Adds pendingInstallFilePath to save params and persists pending fields (pendingInstallFilePath, pendingInstallVersion, pendingInstallAssetName) when saving pending installs.
Package Events & Sync
core/data/.../PackageEventReceiver.kt, core/domain/.../SyncInstalledAppsUseCase.kt
Clears parked-install metadata and deletes parked APK on confirmed installs; adds sweep to clear stale parked-file metadata for already-installed rows.
Strings / Resources
core/presentation/.../strings.xml
Adds discard-pending-install strings and a full set of localized strings for APK Inspect (title, coachmark, sections, protection labels, empty text, etc.).

Sequence Diagram(s)

sequenceDiagram
    participant UI as Compose UI
    participant VM as DetailsViewModel
    participant Inspector as ApkInspector
    participant PM as PackageManager/FS/Repo

    UI->>VM: OnInspectApk
    VM->>VM: set isApkInspectLoading/isApkInspectSheetVisible
    VM->>Inspector: inspectInstalled(package) or inspectFile(path)
    Inspector->>PM: read PackageInfo / APK file
    PM-->>Inspector: PackageInfo / file metadata
    Inspector-->>VM: ApkInspection? (or null)
    VM->>TweaksRepo: set coachmark shown (if needed)
    VM-->>UI: state update -> show sheet / content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hop through APKs with curious paws,

I peek at signatures, labels, and laws.
A pulsing coachmark calls — inspect! — so neat,
A bottom sheet unfolds with bytes and feats.
Hooray — the rabbit found each app's hidden treat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% 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 'feat: APK Inspect feature + faithful pending-row UX' is highly relevant and specific, clearly describing the two main changes in the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/apk-inspect-and-pending-row-polish

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt (1)

2101-2125: ⚠️ Potential issue | 🔴 Critical

Clear pending-install metadata when update completes successfully with isPendingInstall = false.

The updateAppVersion implementation in InstalledAppsRepositoryImpl.kt does not clear pendingInstallFilePath, pendingInstallVersion, and pendingInstallAssetName when the update completes. The app.copy(...) call only explicitly sets isPendingInstall without nulling the parked metadata fields, so they retain their previous values. When combined with the DetailsViewModel code here that only writes pending paths on pending installs, a row that previously had parked metadata from a cancelled install will keep those fields after a successful update, causing the UI to incorrectly display the app as "ready to install" even though it has already been updated.

Code reference
        if (isUpdate) {
            installationManager.updateInstalledAppVersion(
                UpdateInstalledAppParams(
                    apkInfo = apkInfo,
                    assetName = assetName,
                    assetUrl = assetUrl,
                    releaseTag = releaseTag,
                    isPendingInstall = isPending,
                ),
            )
            // For pending updates, also park the file path on the row
            // so the apps list can resume the install in one tap if
            // the user dismissed the system prompt.
            if (pendingPath != null) {
                runCatching {
                    installedAppsRepository.setPendingInstallFilePath(
                        packageName = apkInfo.packageName,
                        path = pendingPath,
                        version = releaseTag,
                        assetName = assetName,
                    )
                }.onFailure { t ->
                    logger.warn("Failed to park pending install path on update: ${t.message}")
                }
            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`
around lines 2101 - 2125, The updateAppVersion flow in
InstalledAppsRepositoryImpl (the method that builds app.copy(...)) must null out
pendingInstallFilePath, pendingInstallVersion, and pendingInstallAssetName when
isPendingInstall is false; modify the updateAppVersion implementation so that
when isPendingInstall == false you set those three fields to null in the
app.copy(...) result (in addition to setting isPendingInstall), ensuring any
previously parked metadata is cleared after a successful update and matches the
DetailsViewModel calls to setPendingInstallFilePath.
🧹 Nitpick comments (3)
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt (1)

66-68: 💤 Low value

LGTM — binding is correct; consider adding imports for consistency.

Minor nit: Lines 66–68 use fully-qualified type names while every other binding in the file uses imported short names. Adding import zed.rainxch.core.domain.system.ApkInspector and import zed.rainxch.core.data.services.DesktopApkInspector would keep the module consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt`
around lines 66 - 68, The binding uses fully-qualified names instead of imports;
add imports for ApkInspector and DesktopApkInspector and replace the
fully-qualified references in the single<...> declaration so the module matches
the project's import style (reference symbols: ApkInspector and
DesktopApkInspector, and the single binding that currently instantiates
DesktopApkInspector).
feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt (1)

69-69: ⚡ Quick win

getPackageArchiveInfo(String, Int) is deprecated since API 33

#getPackageArchiveInfo(String, PackageInfoFlags) should be used when long flags are needed. The int flags parameter was replaced with PackageInfoFlags in API 33. The current call will produce a lint/compiler deprecation warning on targets API 33+. The fix requires a version branch:

♻️ Proposed fix — branch on API level
+import android.os.Build
+import android.content.pm.PackageManager.PackageInfoFlags

         val info = packageManager.getPackageArchiveInfo(apkFilePath, 0)
-        val info = packageManager.getPackageArchiveInfo(apkFilePath, 0)
+        val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            packageManager.getPackageArchiveInfo(apkFilePath, PackageInfoFlags.of(0L))
+        } else {
+            `@Suppress`("DEPRECATION")
+            packageManager.getPackageArchiveInfo(apkFilePath, 0)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt`
at line 69, The call to packageManager.getPackageArchiveInfo(apkFilePath, 0) in
InstalledAppIcon.android.kt is using the deprecated int-flag overload; update it
to branch on API level: for SDK_INT >= 33 call
packageManager.getPackageArchiveInfo(apkFilePath, PackageInfoFlags.of(0)) and
for older SDKs keep packageManager.getPackageArchiveInfo(apkFilePath, 0); locate
the usage of apkFilePath and the getPackageArchiveInfo call inside the
InstalledAppIcon component and replace with a version-guarded call (import
PackageInfoFlags) so the code compiles without deprecation warnings on API 33+.
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt (1)

373-379: 💤 Low value

Prefer an import over the fully-qualified class name for ApkInspectSheet.

ApkInspectSheet is already imported unqualified in Header.kt. Using the fully-qualified name here is inconsistent with every other component reference in this file.

♻️ Proposed fix

Add to the import block (alongside the existing component imports):

+import zed.rainxch.details.presentation.components.ApkInspectSheet

Then simplify the call site:

-        zed.rainxch.details.presentation.components.ApkInspectSheet(
+        ApkInspectSheet(
             inspection = state.apkInspection,
             isLoading = state.isApkInspectLoading,
             onDismiss = { viewModel.onAction(DetailsAction.OnDismissApkInspect) },
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`
around lines 373 - 379, The call site uses a fully-qualified name for
ApkInspectSheet; add an import for
zed.rainxch.details.presentation.components.ApkInspectSheet to the import block
and replace the fully-qualified reference in DetailsRoot (the conditional
guarded by state.isApkInspectSheetVisible) with the simple ApkInspectSheet(...)
call, keeping the same parameters (inspection = state.apkInspection, isLoading =
state.isApkInspectLoading, onDismiss = {
viewModel.onAction(DetailsAction.OnDismissApkInspect) }).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt`:
- Around line 22-71: Both inspectFile and inspectInstalled can let
PackageManager/resource lookup exceptions escape from buildInspection(),
violating the ApkInspector contract; wrap the buildInspection(...) calls in a
try/catch that catches Throwable (or at minimum RuntimeException/Exception) and
on any failure log the error (using Logger.w or Logger.e with TAG) and return
null from the withContext block so callers like
DetailsViewModel.openApkInspectSheet() receive null instead of staying stuck;
specifically modify the calls to buildInspection inside inspectFile and
inspectInstalled to be inside a try { val result = buildInspection(...) ; result
} catch (t: Throwable) { Logger.w(TAG) { "inspect...: failed to extract APK
metadata: $t" } ; null }.

In
`@feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt`:
- Around line 47-58: The function resolveInstalledIcon currently only catches
NameNotFoundException which lets other runtime errors (e.g., SecurityException
from packageManager.getApplicationIcon or exceptions from Drawable.toBitmap)
bubble up into the remember lambda; change its error handling to match
resolveApkIcon by broadening the catch to catch Throwable (or Exception) around
the whole try block so any unexpected exceptions are swallowed and the function
returns null instead of crashing the Composable; update the catch site in
resolveInstalledIcon and ensure you still return null on failure.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt`:
- Around line 1163-1172: The discard IconButton currently calls
onDiscardPendingClick immediately; add a confirmation guard like the existing
appPendingUninstall flow so users must confirm before the parked APK/DB row is
removed: introduce a local boolean state (e.g., showConfirmDiscardPending), show
the same confirmation dialog UI used by appPendingUninstall when the IconButton
is clicked (set showConfirmDiscardPending = true), and only call
onDiscardPendingClick when the user confirms; apply the same pattern to the
legacy full-width "Discard" Button handling the pending rows so both triggers
use the confirmation dialog.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt`:
- Around line 141-143: The computed flag canInspectApk is wrong: replace the
current expression with a check that installedApp exists and either is not a
pending install or has a pendingInstallFilePath; e.g. grab val app =
state.installedApp and set val canInspectApk = app != null &&
(!app.isPendingInstall || app.pendingInstallFilePath != null) so
InspectApkButton is only shown when there is something concrete to inspect.

---

Outside diff comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`:
- Around line 2101-2125: The updateAppVersion flow in
InstalledAppsRepositoryImpl (the method that builds app.copy(...)) must null out
pendingInstallFilePath, pendingInstallVersion, and pendingInstallAssetName when
isPendingInstall is false; modify the updateAppVersion implementation so that
when isPendingInstall == false you set those three fields to null in the
app.copy(...) result (in addition to setting isPendingInstall), ensuring any
previously parked metadata is cleared after a successful update and matches the
DetailsViewModel calls to setPendingInstallFilePath.

---

Nitpick comments:
In `@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt`:
- Around line 66-68: The binding uses fully-qualified names instead of imports;
add imports for ApkInspector and DesktopApkInspector and replace the
fully-qualified references in the single<...> declaration so the module matches
the project's import style (reference symbols: ApkInspector and
DesktopApkInspector, and the single binding that currently instantiates
DesktopApkInspector).

In
`@feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt`:
- Line 69: The call to packageManager.getPackageArchiveInfo(apkFilePath, 0) in
InstalledAppIcon.android.kt is using the deprecated int-flag overload; update it
to branch on API level: for SDK_INT >= 33 call
packageManager.getPackageArchiveInfo(apkFilePath, PackageInfoFlags.of(0)) and
for older SDKs keep packageManager.getPackageArchiveInfo(apkFilePath, 0); locate
the usage of apkFilePath and the getPackageArchiveInfo call inside the
InstalledAppIcon component and replace with a version-guarded call (import
PackageInfoFlags) so the code compiles without deprecation warnings on API 33+.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`:
- Around line 373-379: The call site uses a fully-qualified name for
ApkInspectSheet; add an import for
zed.rainxch.details.presentation.components.ApkInspectSheet to the import block
and replace the fully-qualified reference in DetailsRoot (the conditional
guarded by state.isApkInspectSheetVisible) with the simple ApkInspectSheet(...)
call, keeping the same parameters (inspection = state.apkInspection, isLoading =
state.isApkInspectLoading, onDismiss = {
viewModel.onAction(DetailsAction.OnDismissApkInspect) }).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff95af83-f787-40ed-848f-e0aa5e6d0df9

📥 Commits

Reviewing files that changed from the base of the PR and between 06d9c97 and 4758b2f.

📒 Files selected for processing (27)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/SigningFingerprint.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopApkInspector.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ApkInspection.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/ApkInspector.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/CompactAppRow.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt
  • feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.jvm.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/system/InstallationManagerImpl.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SaveInstalledAppParams.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/InspectApkButton.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt`:
- Around line 206-210: The InspectRow currently combines inspection.versionName
and inspection.versionCode under the label used in ApkInspectSheet, making the
code value ambiguous; change this to render two separate rows: one InspectRow
for the version name (use stringResource for a version name label, e.g.,
Res.string.apk_inspect_version_name) that displays inspection.versionName or "—"
when null, and a second InspectRow for the version code (keep the existing
Res.string.apk_inspect_version_code) that displays
inspection.versionCode?.toString() or "—" when null; update any nearby layout
spacing/ordering so the two rows appear where the combined row was.
- Around line 300-305: The expand/collapse map stored in expanded is created
with remember { mutableStateMapOf(...) } so it survives across inspections;
change the remember key to include the current inspection identity (e.g.,
permissions or an inspectionId) so a new APK re-initializes the map. Concretely,
update the remember call that builds expanded (the mutableStateMapOf populated
via orderedLevels and defaultExpanded) to use remember(permissions) or
remember(inspectionId, permissions) so the map is recreated per inspection and
each APK uses its own defaults.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6aae0b9-61f1-4bfe-affc-49c59e152445

📥 Commits

Reviewing files that changed from the base of the PR and between 4758b2f and bf55816.

📒 Files selected for processing (4)
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt
✅ Files skipped from review due to trivial changes (2)
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/Header.kt

Comment on lines +206 to +210
InspectRow(
label = stringResource(Res.string.apk_inspect_version_code),
value = inspection.versionName?.let { name ->
inspection.versionCode?.let { code -> "$name ($code)" } ?: name
} ?: inspection.versionCode?.toString() ?: "—",
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Split the version name and version code display.

versionName and versionCode are separate fields, but this row merges them under one label. When both are present, the sheet no longer shows a clear version code value on its own, which makes build comparisons ambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt`
around lines 206 - 210, The InspectRow currently combines inspection.versionName
and inspection.versionCode under the label used in ApkInspectSheet, making the
code value ambiguous; change this to render two separate rows: one InspectRow
for the version name (use stringResource for a version name label, e.g.,
Res.string.apk_inspect_version_name) that displays inspection.versionName or "—"
when null, and a second InspectRow for the version code (keep the existing
Res.string.apk_inspect_version_code) that displays
inspection.versionCode?.toString() or "—" when null; update any nearby layout
spacing/ordering so the two rows appear where the combined row was.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt`:
- Around line 131-140: In clearParkedInstall (PackageEventReceiver), don't treat
File.delete() as infallible and avoid clearing DB metadata before confirming
file removal: if parkedFilePath is non-null, first check
java.io.File(parkedFilePath).exists() and call delete(), inspect the boolean
result, log a warning if it returns false and skip/abort calling
repo.setPendingInstallFilePath(packageName = packageName, path = null); only
when delete() returns true (or file didn't exist) call
repo.setPendingInstallFilePath to clear the metadata, and preserve the existing
runCatching/onFailure logging around repo.setPendingInstallFilePath (or include
the caught exception) so partial cleanup outcomes are explicit.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt`:
- Around line 124-133: The clears of parked-file metadata
(installedAppsRepository.setPendingInstallFilePath) are racy because they use a
local executeInTransaction wrapper and unconditionally set path=null; change
this to use the repository-backed transactional API (use the repository's real
transaction method instead of local executeInTransaction) and perform a
conditional/CAS-style update: read the current pending path inside that
repository transaction and only setPendingInstallFilePath(packageName, null) if
the current value still equals the expected stale value (the value you observed
earlier); apply the same pattern to the other clear block referenced (lines
around 138-152) so concurrent writes cannot be clobbered.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 711ff01c-7a20-4ca0-b578-93ddffede015

📥 Commits

Reviewing files that changed from the base of the PR and between bf55816 and 467ce40.

📒 Files selected for processing (2)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt

Comment on lines +131 to +140
runCatching {
repo.setPendingInstallFilePath(packageName = packageName, path = null)
}.onFailure {
Logger.w(it) { "Failed to clear parked install metadata for $packageName" }
}
if (parkedFilePath != null) {
runCatching { java.io.File(parkedFilePath).takeIf { it.exists() }?.delete() }
.onFailure {
Logger.w(it) { "Failed to delete parked APK at $parkedFilePath" }
}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle partial cleanup outcomes explicitly in clearParkedInstall.

delete() can fail by returning false (no exception), so this path can silently skip cleanup. Also, deleting the file after a failed metadata clear can leave DB state pointing to a non-existent file.

Suggested patch
-        runCatching {
-            repo.setPendingInstallFilePath(packageName = packageName, path = null)
-        }.onFailure {
+        val metadataCleared = runCatching {
+            repo.setPendingInstallFilePath(packageName = packageName, path = null)
+        }.onFailure {
             Logger.w(it) { "Failed to clear parked install metadata for $packageName" }
-        }
-        if (parkedFilePath != null) {
-            runCatching { java.io.File(parkedFilePath).takeIf { it.exists() }?.delete() }
+        }.isSuccess
+        if (metadataCleared && parkedFilePath != null) {
+            runCatching {
+                val file = java.io.File(parkedFilePath)
+                if (file.exists() && !file.delete()) {
+                    Logger.w { "Failed to delete parked APK at $parkedFilePath" }
+                }
+            }
                 .onFailure {
                     Logger.w(it) { "Failed to delete parked APK at $parkedFilePath" }
                 }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt`
around lines 131 - 140, In clearParkedInstall (PackageEventReceiver), don't
treat File.delete() as infallible and avoid clearing DB metadata before
confirming file removal: if parkedFilePath is non-null, first check
java.io.File(parkedFilePath).exists() and call delete(), inspect the boolean
result, log a warning if it returns false and skip/abort calling
repo.setPendingInstallFilePath(packageName = packageName, path = null); only
when delete() returns true (or file didn't exist) call
repo.setPendingInstallFilePath to clear the metadata, and preserve the existing
runCatching/onFailure logging around repo.setPendingInstallFilePath (or include
the caught exception) so partial cleanup outcomes are explicit.

Comment on lines +124 to 133
// Resolution implies the system holds the
// package — drop the parked-file metadata
// so the apps row stops advertising an
// Install CTA on a file the user already
// installed.
installedAppsRepository.setPendingInstallFilePath(
packageName = app.packageName,
path = null,
)
} catch (e: Exception) {
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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

These parked-metadata clears are vulnerable to lost updates under concurrency.

Both clears use stale snapshot data and run under a local executeInTransaction wrapper that does not provide real DB isolation. A concurrent writer can set a fresh parked path while this sweep clears it back to null, causing state regression.

Use a true repository-backed transaction and add a conditional/CAS-style clear (e.g., clear only if current row still matches expected pending state/path) to avoid clobbering newer writes.

Also applies to: 138-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt`
around lines 124 - 133, The clears of parked-file metadata
(installedAppsRepository.setPendingInstallFilePath) are racy because they use a
local executeInTransaction wrapper and unconditionally set path=null; change
this to use the repository-backed transactional API (use the repository's real
transaction method instead of local executeInTransaction) and perform a
conditional/CAS-style update: read the current pending path inside that
repository transaction and only setPendingInstallFilePath(packageName, null) if
the current value still equals the expected stale value (the value you observed
earlier); apply the same pattern to the other clear block referenced (lines
around 138-152) so concurrent writes cannot be clobbered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt`:
- Around line 159-163: The current granted expression is a no-op; update
resolvePermission to accept an installed-package flag (e.g., add a parameter
like isInstalledPackage) and implement the intended logic there: if protection
== ProtectionLevel.NORMAL then return true when isInstalledPackage is true and
return null for file-only; otherwise preserve existing granted handling. Then
update the call site in buildInspection to pass the installation context
(isInstalledPackage) into resolvePermission so NORMAL permissions are
auto-granted for installed packages and remain null for file-only artifacts.
- Around line 62-88: The try/catch around pm.getPackageInfoCompat in
inspectInstalled is currently only catching PackageManager.NameNotFoundException
which lets other runtime/binder/security exceptions escape; change that call to
catch Throwable (or at least Exception) so any unexpected errors from
getPackageInfoCompat (e.g., SecurityException, DeadObjectException) are caught,
log the throwable with Logger.w(TAG) including the packageName and exception
details, and return null (same behavior as for NameNotFoundException) so the
coroutine doesn’t propagate unexpected PM exceptions; keep the rest of
inspectInstalled (sourceDir, sizeBytes, buildInspection) unchanged.

In
`@feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt`:
- Around line 1471-1479: The catch block in discardPendingInstall swallows
failures from installedAppsRepository.deleteInstalledApp and leaves the DB row
pointing to a deleted file; update the catch to mirror uninstallApp's behavior:
rethrow CancellationException, and for other Throwables log the error and
dispatch an AppsEvent.ShowError (including app.packageName and t.message) so the
UI shows feedback; locate the code in AppsViewModel around
discardPendingInstall/deleteInstalledApp and replace the current logger-only
catch with a call to the same event/emission mechanism used by uninstallApp
while preserving the existing log entry.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4352bdf1-53a6-4fd3-a9f5-dbb4098f9254

📥 Commits

Reviewing files that changed from the base of the PR and between 467ce40 and 9de8a0f.

📒 Files selected for processing (12)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidApkInspector.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt
  • feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt
✅ Files skipped from review due to trivial changes (1)
  • core/presentation/src/commonMain/composeResources/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (2)
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/ApkInspectSheet.kt

@rainxchzed rainxchzed merged commit 44317fb into main May 3, 2026
1 check was pending
@rainxchzed rainxchzed deleted the feat/apk-inspect-and-pending-row-polish branch May 3, 2026 05:31
@coderabbitai coderabbitai Bot mentioned this pull request May 4, 2026
8 tasks
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