Skip to content

Conversation

@rainxchzed
Copy link
Owner

@rainxchzed rainxchzed commented Jan 16, 2026

This commit introduces support for silent application installations and updates on Android using Shizuku. When Shizuku is active and permission is granted, installations will proceed in the background without requiring user interaction. This also enables a new auto-update feature for installed applications.

  • feat(shizuku): Integrated Shizuku for silent APK installation, including permission requests and status checks.
  • feat(shizuku): Implemented ShizukuInstaller and ShizukuManager to handle the installation logic and binder lifecycle.
  • feat(shizuku): Added a BroadcastReceiver to handle installation results from the Android Package Installer.
  • feat(shizuku): Introduced an AutoUpdateWorker to periodically check for and install updates in the background for apps with auto-update enabled.
  • refactor(installer): Moved AndroidInstaller to a dedicated installer package and refactored it to use the new Shizuku services as the primary installation method, with a fallback to the standard package installer.
  • feat(ui): The UI now indicates when Shizuku is active and displays installation progress for silent installs.
  • feat(ui): Added an "auto-update" toggle switch on the details screen for installed apps when Shizuku is available.
  • feat(ui): Added options in the installer menu to guide users through Shizuku setup.
  • chore(deps): Added Shizuku API and AndroidX Work Manager dependencies.
  • chore(android): Updated AndroidManifest.xml with permissions and components required for Shizuku and background work.

Summary by CodeRabbit

  • New Features

    • Shizuku support for silent, elevated app installs and uninstalls on Android
    • In-app setup dialog to guide installation, start, and permission steps
    • Per-installation progress reporting (detailed stages) and ability to open Shizuku
  • Enhancements

    • Auto-update support with attempt history and failure tracking
    • UI updates: progress indicators, Shizuku status badge, and auto-update toggle
  • Documentation

    • Added numerous user-facing strings for Shizuku setup, silent install, and auto-update

✏️ Tip: You can customize this high-level summary in your review settings.

This commit introduces support for silent application installations and updates on Android using Shizuku. When Shizuku is active and permission is granted, installations will proceed in the background without requiring user interaction. This also enables a new auto-update feature for installed applications.

- **feat(shizuku)**: Integrated Shizuku for silent APK installation, including permission requests and status checks.
- **feat(shizuku)**: Implemented `ShizukuInstaller` and `ShizukuManager` to handle the installation logic and binder lifecycle.
- **feat(shizuku)**: Added a `BroadcastReceiver` to handle installation results from the Android Package Installer.
- **feat(shizuku)**: Introduced an `AutoUpdateWorker` to periodically check for and install updates in the background for apps with auto-update enabled.
- **refactor(installer)**: Moved `AndroidInstaller` to a dedicated `installer` package and refactored it to use the new Shizuku services as the primary installation method, with a fallback to the standard package installer.
- **feat(ui)**: The UI now indicates when Shizuku is active and displays installation progress for silent installs.
- **feat(ui)**: Added an "auto-update" toggle switch on the details screen for installed apps when Shizuku is available.
- **feat(ui)**: Added options in the installer menu to guide users through Shizuku setup.
- **chore(deps)**: Added Shizuku API and AndroidX Work Manager dependencies.
- **chore(android)**: Updated `AndroidManifest.xml` with permissions and components required for Shizuku and background work.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Walkthrough

Adds Shizuku-based silent installation support: new ShizukuManager, ShizukuInstaller, receiver, installer interface extensions, UI/setup flows, DI wiring, manifest permissions, resources, data-model changes, and build/dependency updates.

Changes

Cohort / File(s) Summary
Shizuku core
composeApp/src/androidMain/kotlin/.../installer/shizuku/ShizukuManager.kt, .../ShizukuInstaller.kt, .../InstallResultReceiver.kt
New Shizuku integration layer: manager (StateFlow status/permission), installer (PackageInstaller session, Flow, commit/wait via broadcast callbacks), and BroadcastReceiver to surface async results.
Installer API & platform impl
composeApp/src/commonMain/kotlin/.../core/data/services/Installer.kt, composeApp/src/androidMain/.../AndroidInstaller.kt, composeApp/src/jvmMain/.../DesktopInstaller.kt
Installer interface extended with Shizuku methods; AndroidInstaller gains shizukuManager/shizukuInstaller constructor deps and dual-path install logic; DesktopInstaller stubs Shizuku API as unavailable.
DI, build and libs
composeApp/src/androidMain/kotlin/.../app/di/PlatformModules.android.kt, composeApp/build.gradle.kts, gradle/libs.versions.toml
Wires ShizukuManager/ShizukuInstaller into DI, updates AndroidInstaller provider, adds dependencies: dev.rikka.shizuku:api, dev.rikka.shizuku:provider, and androidx.work:work-runtime-ktx.
Android manifest
composeApp/src/androidMain/AndroidManifest.xml
Added permissions (moe.shizuku.manager.permission.API_V23, RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE) and registered non-exported InstallResultReceiver with install/uninstall intent-filters.
UI state & flows
composeApp/src/commonMain/kotlin/.../feature/details/presentation/{DetailsViewModel.kt,DetailsState.kt,DetailsAction.kt,DetailsRoot.kt}
Adds Shizuku-related state, actions, dialogs and wiring; installation flow updated to consume InstallationProgress and show setup dialog/permission flows.
UI components
composeApp/src/commonMain/kotlin/.../components/{ShizukuSetupDialog.kt,SmartInstallButton.kt,sections/Header.kt}
New ShizukuSetupDialog, SmartInstallButton shows Shizuku badge/progress/auto-update toggle, Header integrates setup option and passes installProgress.
Models & DB
composeApp/src/commonMain/kotlin/.../core/data/model/InstallationProgress.kt, .../local/db/entities/InstalledApp.kt
New sealed InstallationProgress type; InstalledApp schema adds auto-update tracking fields and adjusts nullable/non-null types.
Resources & migration
composeApp/src/commonMain/composeResources/values/strings.xml, composeApp/src/commonMain/kotlin/.../MainViewModel.kt
Added ~22 Shizuku strings; MainViewModel adds migration/sync flow to reconcile installed apps and system package info.
Minor repo & formatting
composeApp/src/commonMain/kotlin/.../core/data/repository/InstalledAppsRepositoryImpl.kt, .../domain/repository/InstalledAppsRepository.kt
Minor formatting changes; no functional differences.

Sequence Diagram(s)

sequenceDiagram
    participant UI as DetailsUI
    participant VM as DetailsViewModel
    participant SI as ShizukuInstaller
    participant PM as PackageManager
    participant BR as InstallResultReceiver
    participant App as User

    UI->>VM: Trigger Install/Update
    activate VM
    VM->>SI: installApk(file) (returns Flow<InstallationProgress>)
    activate SI
    SI->>SI: ensurePermissionsOrThrow()
    SI->>PM: createPackageInstallerSession()
    PM-->>SI: sessionId
    SI->>SI: write APK stream -> emits Installing(progress)
    SI-->>VM: emits progress
    VM-->>UI: update progress
    SI->>PM: commitSession(sessionId)
    PM->>BR: broadcast ACTION_INSTALL_RESULT (async)
    BR->>SI: onInstallResult(sessionId, success, pkg, error)
    SI-->>VM: emit Success/Error, complete flow
    VM->>VM: update DB/state
    deactivate VM
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hop where bolts of progress gleam,
Silent installs, a coder's dream,
Permissions granted, sessions stream,
A tiny rabbit cheers the scheme! ⚡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: adding support for silent installs via Shizuku on Android.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

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

This commit integrates Shizuku to enable silent installations, updates, and reinstalls directly from the app details screen. It also introduces an auto-update feature for installed applications, which has been removed for now.

- **feat(Shizuku)**: Added UI components to the details screen to show Shizuku status and enable silent operations (`Silent install`, `Silent update`, `Silent reinstall`).
- **refactor**: Removed the `AutoUpdateWorker` and its related functionality.
- **i18n**: Added new strings for Shizuku integration and auto-update features.
Copy link
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: 10

Caution

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

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

71-77: Wire OnNavigateToShizukuSetup to a real navigation callback.

The empty handler makes the action a no-op, so the new UI path won’t do anything. Add a callback parameter and invoke it here (and update call sites accordingly).

🛠️ Suggested fix
 fun DetailsRoot(
     onNavigateBack: () -> Unit,
     onNavigateToDeveloperProfile: (username: String) -> Unit,
     onOpenRepositoryInApp: (repoId: Long) -> Unit,
+    onNavigateToShizukuSetup: () -> Unit,
     viewModel: DetailsViewModel = koinViewModel()
 ) {
@@
             DetailsEvent.OnNavigateToShizukuSetup -> {
-
+                onNavigateToShizukuSetup()
             }

Also applies to: 98-100

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (1)

195-233: isShizukuEnabled has misleading semantics—it should reflect actual permission state or be renamed.

Currently set to platform.type == PlatformType.ANDROID, which is always true on Android regardless of whether Shizuku is installed, running, or has permission. The variable name implies an enabled/permission state, but it only checks the platform type. Since installer.isShizukuAvailable() already provides the real state (requires both isAvailable.value && hasPermission.value), either use that directly for isShizukuEnabled or rename to isShizukuSupportedOnPlatform to clarify the intent.

🔧 Possible fix
-                val isShizukuEnabled = platform.type == PlatformType.ANDROID
+                val isShizukuEnabled = isShizukuAvailable
🤖 Fix all issues with AI agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt`:
- Around line 48-52: Replace the incorrect DI context retrieval for
ShizukuManager: the single<ShizukuManager> provider currently constructs
ShizukuManager with context = get(), which fails because no Context binding is
registered; update it to use androidContext() (i.e., change the constructor arg
in the single<ShizukuManager> block to context = androidContext()) to match
other providers and ensure the Android Context is injected correctly.

In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt`:
- Around line 151-158: install() currently calls installViaShizuku(file) which
returns a Flow<InstallationProgress> but never collects it, so the installation
never runs; update install() to either collect the flow (e.g., call
installViaShizuku(file).collect { /* handle progress/errors */ } within a
coroutine) or explicitly start it fire-and-forget using flow.launchIn(someScope)
after injecting a CoroutineScope into the AndroidInstaller; ensure you handle
progress updates and errors the same way you do for installViaStandard(file).

In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt`:
- Around line 8-16: The callback registry maps in ShizukuInstaller (the
mutableMapOf instances used to track install/uninstall callbacks referenced near
the top of the file and at the locations around lines 284-285) are not
thread-safe; replace those mutableMapOf declarations with
java.util.concurrent.ConcurrentHashMap instances (e.g., val myCallbacks:
MutableMap<Key,Value> = ConcurrentHashMap()) and add the import for
java.util.concurrent.ConcurrentHashMap, and when iterating over those registries
in install/uninstall coroutines or broadcast handlers use a safe snapshot (e.g.,
.values.toList() or ConcurrentHashMap.forEach) instead of iterating the map
directly to avoid concurrent modification races.
- Around line 213-239: The uninstall callback stored in uninstallCallbacks
inside the suspendCancellableCoroutine block can leak if the coroutine is
cancelled or an exception occurs; update the suspendCancellableCoroutine usage
in ShizukuInstaller.kt so that after registering uninstallCallbacks[packageName]
= { ... } you (1) register continuation.invokeOnCancellation {
uninstallCallbacks.remove(packageName) } to remove the callback on cancellation
and (2) wrap the setup in try/catch/finally (or ensure a failure path) that
removes uninstallCallbacks.remove(packageName) when an exception is thrown
before completion, ensuring the callback is always removed in all paths that
exit suspendCancellableCoroutine prematurely.
- Around line 105-127: The code currently reopens the entry with
session.openWrite("base.apk", 0, 0) and calls session.fsync on that new stream
(causing a leaked descriptor) instead of fsync-ing the original write stream;
fix this in ShizukuInstaller.kt by moving the session.fsync call into the
existing session.openWrite(...).use { outputStream -> ... } block right after
outputStream.flush(), calling session.fsync(outputStream) on that same
outputStream, and remove the later session.openWrite("base.apk", 0, 0) /
session.fsync(...) call.

In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt`:
- Around line 43-48: The ShizukuManager registers listeners in its init
(binderReceivedListener, binderDeadListener, permissionResultListener) but never
calls cleanup(), causing a listener leak; update app DI or lifecycle to invoke
ShizukuManager.cleanup() on shutdown—either add an onClose callback to the Koin
single that constructs ShizukuManager (call get<ShizukuManager>().cleanup() in
onClose) or wire cleanup() into your Application lifecycle (e.g., in onTerminate
or a LifecycleObserver) so the listeners are unregistered when the app / Koin
context is closed.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt`:
- Around line 18-21: The InstalledApp entity was extended with
autoUpdateEnabled, lastAutoUpdateAttempt, autoUpdateFailCount, and
autoUpdateFailReason but no Room migration to version 4 exists; add/upgrade the
DB version constant to 4 and implement MIGRATION_3_4 (referenced where existing
MIGRATION_1_2 and MIGRATION_2_3 are registered, e.g., initDatabase.kt) that
executes ALTER TABLE ... ADD COLUMN statements for the four new fields (or
annotate the InstalledApp properties with `@ColumnInfo`(defaultValue = "...") and
ensure the SQL defaults match to avoid NOT NULL issues), then register
MIGRATION_3_4 alongside the other migrations so Room schema validation succeeds
on upgrade.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt`:
- Around line 415-449: Replace the Row's Modifier.clickable handler with
Modifier.toggleable(...) so the whole row handles toggling via toggleable(role =
Role.Switch, checked = installedApp.autoUpdateEnabled, onValueChange = {
onAction(DetailsAction.ToggleAutoUpdate) }) and make the Switch read‑only by
removing its onCheckedChange (leave checked = installedApp.autoUpdateEnabled and
no onCheckedChange) so the action is only fired once; update imports to include
androidx.compose.foundation.selection.toggleable and
androidx.compose.ui.semantics.Role if needed and keep references to
DetailsAction.ToggleAutoUpdate, installedApp.autoUpdateEnabled, and
SmartInstallButton.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`:
- Around line 517-533: The RequestShizukuPermission branch in DetailsViewModel
currently treats installer.requestShizukuPermission() as a synchronous result
and only updates _state when permission was already granted; instead, subscribe
to the shizukuManager.hasPermission StateFlow (or equivalent permission-change
callback) inside the ViewModel (e.g., in init or when handling
DetailsAction.RequestShizukuPermission) using viewModelScope.launch/collect to
update _state.copy(isShizukuAvailable = installer.isShizukuAvailable()) whenever
permission changes, keep sending the OnMessage event for the immediate false
return if you still want to show the dialog fired, and remove relying on the
immediate boolean to reflect the final permission state so the UI refreshes when
the async dialog result arrives.
- Around line 535-555: The ToggleAutoUpdate handler lacks error handling for
repository calls; wrap the operations inside viewModelScope.launch for
DetailsAction.ToggleAutoUpdate in a try/catch, catch any Exception thrown by
installedAppsRepository.updateApp or getAppByPackage, log the error using the
same logger used elsewhere in this ViewModel (e.g., logger.error or similar),
and send a user-facing failure message via
_events.send(DetailsEvent.OnMessage(...)) instead of updating _state when an
error occurs; keep the successful-path behavior (updating app and sending
enabled/disabled message) inside the try block.
🧹 Nitpick comments (4)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/InstallResultReceiver.kt (1)

11-26: Guard against missing sessionId before forwarding results.

If the intent is missing the session ID, passing -1 downstream can misroute or pollute state. Consider early return with a log.

Suggested defensive guard
                 val sessionId = intent.getIntExtra(ShizukuInstaller.EXTRA_SESSION_ID, -1)
+                if (sessionId == -1) {
+                    Logger.w { "Install result missing sessionId; ignoring." }
+                    return
+                }
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt (1)

35-45: Avoid crashing non-Android callers when Shizuku is unavailable.

If any shared UI path calls installWithShizukuProgress without gating, this will throw. Please verify all call sites check isShizukuAvailable/isShizukuInstalled, or consider returning a failure Flow instead.

Optional safer default
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
@@
-    fun installWithShizukuProgress(file: File): Flow<InstallationProgress> {
-        throw UnsupportedOperationException("Shizuku installation not supported on this platform")
-    }
+    fun installWithShizukuProgress(file: File): Flow<InstallationProgress> =
+        flowOf(InstallationProgress.Error("Shizuku installation not supported on this platform"))
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt (1)

161-167: Consider removing this wrapper method.

installViaShizuku simply delegates to installWithShizukuProgress without adding any behavior. After fixing the flow collection issue in install(), this wrapper becomes unnecessary.

♻️ Suggested cleanup
-    /**
-     * Installs APK using Shizuku for silent installation.
-     * Returns a Flow that emits installation progress.
-     */
-    private fun installViaShizuku(file: File): Flow<InstallationProgress> {
-        return installWithShizukuProgress(file)
-    }

And call installWithShizukuProgress(file) directly in install().

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt (1)

112-118: Consider logging the exception for debugging.

The static analysis tool flagged that the exception is swallowed. While returning -1 is a reasonable fallback, logging the exception would help with debugging if users report version-related issues.

♻️ Suggested improvement
     fun getShizukuVersion(): Int {
         return try {
             Shizuku.getVersion()
         } catch (e: Exception) {
+            Logger.w(e) { "Failed to get Shizuku version" }
             -1
         }
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a5bdaa and 7590006.

📒 Files selected for processing (20)
  • composeApp/build.gradle.kts
  • composeApp/src/androidMain/AndroidManifest.xml
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/InstallResultReceiver.kt
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt
  • composeApp/src/commonMain/composeResources/values/strings.xml
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/model/InstallationProgress.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsState.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/sections/Header.kt
  • composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt
  • gradle/libs.versions.toml
🧰 Additional context used
🧬 Code graph analysis (3)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt (2)
  • isShizukuAvailable (35-35)
  • installWithShizukuProgress (41-43)
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt (2)
  • isShizukuAvailable (952-952)
  • installWithShizukuProgress (956-958)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt (2)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/utils/SystemArchitecture.kt (2)
  • extractArchitectureFromName (5-14)
  • isExactArchitectureMatch (16-25)
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt (1)
  • isExactArchitectureMatch (299-311)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt (2)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/data_store/createDataStore.kt (1)
  • createDataStore (7-11)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/db/initDatabase.kt (1)
  • initDatabase (9-23)
🪛 detekt (1.23.8)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt

[warning] 107-107: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 115-115: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (26)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt (5)

1-31: LGTM!

Imports are properly organized with the new Shizuku-related classes and the reorganized installer package structure.


34-39: LGTM!

Correct use of androidContext() for context injection.


54-61: LGTM!

The new Shizuku dependencies are properly wired into AndroidInstaller for the silent installation feature.


63-109: LGTM!

Consistent use of androidContext() for context injection across all services. The refactoring to explicit named parameters improves readability.


111-119: LGTM!

The remaining service bindings are correctly configured.

composeApp/build.gradle.kts (2)

77-80: Confirm Shizuku/WorkManager dependency versions and scopes.

Looks fine; please verify the pinned versions are valid and align with official docs for app modules, and that the chosen scopes match the recommended setup.


165-168: LGTM: KSP/JVM tasks depend on BuildConfig generation.

Nice improvement to ensure generated config is available for matching tasks.

gradle/libs.versions.toml (2)

12-39: Verify new version pins (Shizuku, WorkManager).

Please confirm these versions exist and are the intended ones for your AGP/Kotlin/targetSdk combination.


105-108: Confirm Shizuku/WorkManager coordinates match official artifacts.

Double-check artifact names/coordinates against the latest docs to avoid resolution surprises.

composeApp/src/androidMain/AndroidManifest.xml (2)

10-14: Re-check necessity of BOOT_COMPLETED and FOREGROUND_SERVICE.

If auto-update or foreground work was removed, consider dropping these permissions to reduce policy surface. Please verify a component truly needs them.


63-70: Ensure install/uninstall results are delivered with exported=false receiver.

With exported="false", delivery must be via an explicit intent/PendingIntent created by your app. Please confirm that the sender uses an explicit target; otherwise results may be dropped.

composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt (1)

952-960: LGTM! Appropriate stub implementations for desktop platform.

The Shizuku API surface is correctly stubbed out for the desktop target, returning false for availability checks and throwing UnsupportedOperationException for the installation flow. This maintains API parity with the common Installer interface while clearly indicating Shizuku is Android-only.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/model/InstallationProgress.kt (1)

3-10: LGTM! Well-structured sealed class for installation progress.

The sealed class provides a clean, exhaustive representation of installation states. Using object for stateless states and data class for stateful ones is idiomatic Kotlin. This design enables type-safe when expressions in consuming code.

composeApp/src/commonMain/composeResources/values/strings.xml (1)

326-337: LGTM! Clear and consistent string resources for Shizuku UI.

The new strings follow existing naming conventions and provide appropriate labels for the Shizuku integration UI states and actions.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt (1)

258-301: LGTM! Clean Shizuku interface implementation.

The Shizuku methods correctly delegate to the injected manager and installer components. The progress mapping in installWithShizukuProgress is exhaustive, and uninstallWithShizuku properly guards against unavailability.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt (2)

103-110: Appropriate exception handling.

The NameNotFoundException is expected when Shizuku isn't installed. Returning false is the correct behavior here. The static analysis warning is a false positive in this context.


11-71: LGTM! Well-structured Shizuku lifecycle management.

The ShizukuManager class provides a clean reactive API with StateFlow for observing Shizuku availability and permission state. The listener setup follows the Shizuku API conventions, and the availability check handles exceptions gracefully with appropriate state updates.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt (1)

23-58: Precondition checks and progress mapping look solid.

Good early exits for missing prerequisites and a clear mapping of internal progress to InstallProgress.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt (1)

3-8: New navigation event integrates cleanly into the sealed hierarchy.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsState.kt (1)

56-59: State additions are clear and scoped.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/sections/Header.kt (1)

74-79: Conditional install UI additions align with the new state flags.

Nice integration of install progress into the button plus the setup option and active-status card.

Also applies to: 91-129, 192-240

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt (1)

15-17: New action variants are well-scoped.

Grouping Shizuku and navigation actions keeps the sealed interface organized and easy to extend.

Also applies to: 24-27, 29-31

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (2)

8-12: Import additions look fine.

Also applies to: 34-46


594-735: Install flow state handling looks consistent.

Progress updates, stage transitions, and final resets are wired cleanly across the new path.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt (2)

11-88: Install-progress param + Shizuku badge integrate cleanly.

Also applies to: 131-155


102-412: Download/install state rendering is cohesive.

The stage-specific UI, progress text, and CTA layout are consistent with the new state model.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +48 to +52
single<ShizukuManager> {
ShizukuManager(
context = get()
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: get() should be androidContext() for context injection.

ShizukuManager uses context = get() which attempts to resolve a Context dependency from the DI graph, but no such binding exists. This will cause a runtime exception when the dependency is resolved. All other services in this module correctly use androidContext().

🐛 Proposed fix
     single<ShizukuManager> {
         ShizukuManager(
-            context = get()
+            context = androidContext()
         )
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
single<ShizukuManager> {
ShizukuManager(
context = get()
)
}
single<ShizukuManager> {
ShizukuManager(
context = androidContext()
)
}
🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt`
around lines 48 - 52, Replace the incorrect DI context retrieval for
ShizukuManager: the single<ShizukuManager> provider currently constructs
ShizukuManager with context = get(), which fails because no Context binding is
registered; update it to use androidContext() (i.e., change the constructor arg
in the single<ShizukuManager> block to context = androidContext()) to match
other providers and ensure the Android Context is injected correctly.

Comment on lines +151 to +158
// Try Shizuku installation first if available
if (isShizukuAvailable()) {
Logger.d { "Using Shizuku for installation" }
installViaShizuku(file)
} else {
Logger.d { "Using standard installation" }
installViaStandard(file)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Flow returned by installViaShizuku is never collected.

The install() method calls installViaShizuku(file) which returns a Flow<InstallationProgress>, but this flow is never collected. Since Kotlin flows are cold, the installation will not execute at all when Shizuku is available.

🐛 Proposed fix

Either collect the flow and handle progress/errors:

     override suspend fun install(filePath: String, extOrMime: String) {
         val file = File(filePath)
         if (!file.exists()) {
             throw IllegalStateException("APK file not found: $filePath")
         }

         Logger.d { "Installing APK: $filePath" }

         // Try Shizuku installation first if available
         if (isShizukuAvailable()) {
             Logger.d { "Using Shizuku for installation" }
-            installViaShizuku(file)
+            installWithShizukuProgress(file).collect { progress ->
+                when (progress) {
+                    is InstallationProgress.Error -> throw IllegalStateException(progress.message)
+                    is InstallationProgress.Success -> Logger.d { "Shizuku installation completed: ${progress.packageName}" }
+                    else -> Logger.d { "Shizuku installation progress: $progress" }
+                }
+            }
         } else {
             Logger.d { "Using standard installation" }
             installViaStandard(file)
         }
     }

Or if the intent is fire-and-forget for Shizuku (since it's silent), use launchIn with a scope, though this would require injecting a CoroutineScope.

🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt`
around lines 151 - 158, install() currently calls installViaShizuku(file) which
returns a Flow<InstallationProgress> but never collects it, so the installation
never runs; update install() to either collect the flow (e.g., call
installViaShizuku(file).collect { /* handle progress/errors */ } within a
coroutine) or explicitly start it fire-and-forget using flow.launchIn(someScope)
after injecting a CoroutineScope into the AndroidInstaller; ensure you handle
progress updates and errors the same way you do for installViaStandard(file).

Comment on lines +8 to +16
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import kotlin.coroutines.resume
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make callback registries thread-safe.

These maps are mutated from install coroutines and broadcast threads. mutableMapOf is not thread-safe, which can cause races or lost callbacks under concurrent installs/uninstalls.

🛠️ Suggested fix
 import kotlinx.coroutines.withContext
 import java.io.File
 import java.io.FileInputStream
+import java.util.concurrent.ConcurrentHashMap
 import kotlin.coroutines.resume
@@
-        internal val installCallbacks = mutableMapOf<Int, (Boolean, String?, String?) -> Unit>()
-        internal val uninstallCallbacks = mutableMapOf<String, (Boolean, String) -> Unit>()
+        internal val installCallbacks = ConcurrentHashMap<Int, (Boolean, String?, String?) -> Unit>()
+        internal val uninstallCallbacks = ConcurrentHashMap<String, (Boolean, String) -> Unit>()

Also applies to: 284-285

🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt`
around lines 8 - 16, The callback registry maps in ShizukuInstaller (the
mutableMapOf instances used to track install/uninstall callbacks referenced near
the top of the file and at the locations around lines 284-285) are not
thread-safe; replace those mutableMapOf declarations with
java.util.concurrent.ConcurrentHashMap instances (e.g., val myCallbacks:
MutableMap<Key,Value> = ConcurrentHashMap()) and add the import for
java.util.concurrent.ConcurrentHashMap, and when iterating over those registries
in install/uninstall coroutines or broadcast handlers use a safe snapshot (e.g.,
.values.toList() or ConcurrentHashMap.forEach) instead of iterating the map
directly to avoid concurrent modification races.

Comment on lines +105 to +127
session.openWrite("base.apk", 0, fileSize).use { outputStream ->
FileInputStream(apkFile).use { inputStream ->
val buffer = ByteArray(65536)
var read: Int

while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
totalWritten += read

val progress = ((totalWritten.toFloat() / fileSize) * 100).toInt()
if (progress >= lastEmittedProgress + 5) {
onProgress(SessionProgress.Writing(progress))
lastEmittedProgress = progress
}
}

outputStream.flush()
}
}

onProgress(SessionProgress.Writing(100))
session.fsync(session.openWrite("base.apk", 0, 0))

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ShizukuInstaller.kt" -type f

Repository: rainxchzed/Github-Store

Length of output: 183


🏁 Script executed:

cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '100,135p'

Repository: rainxchzed/Github-Store

Length of output: 1616


🏁 Script executed:

# Search for other usages of session.openWrite or session.fsync in the codebase
rg "session\.(openWrite|fsync)" --type kt -B 2 -A 2

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

rg "session\.(openWrite|fsync)" -B 2 -A 2

Repository: rainxchzed/Github-Store

Length of output: 1618


🏁 Script executed:

# Look at imports and see what type of session this is
cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | head -50

Repository: rainxchzed/Github-Store

Length of output: 2336


🏁 Script executed:

cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '60,105p'

Repository: rainxchzed/Github-Store

Length of output: 2320


🏁 Script executed:

cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '125,145p'

Repository: rainxchzed/Github-Store

Length of output: 927


🌐 Web query:

Android PackageInstaller Session fsync openWrite documentation proper usage

💡 Result:

Short answer — use Session.openWrite(...) to get the OutputStream, write your APK bytes, call Session.fsync(outputStream) (optional but recommended to force persistence), close the stream, then call Session.commit(...). fsync only accepts streams returned by openWrite and ensures outstanding bytes are committed to disk; all streams must be closed before commit. [1][2]

Minimal usage pattern (Java-like):

  • Session session = packageInstaller.openSession(sessionId);
  • OutputStream out = session.openWrite(name, 0, apkLength);
  • write bytes to out (loop/writeFully)
  • session.fsync(out); // ensure data is flushed to disk (only valid for openWrite streams)
  • out.close();
  • session.commit(statusReceiver);
  • session.close();

References:

  • PackageInstaller.Session — openWrite, fsync, commit (Android Developers). [1]
  • PackageInstaller.Session Java reference (mirror/alternate). [2]

🏁 Script executed:

cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '145,170p'

Repository: rainxchzed/Github-Store

Length of output: 1046


🏁 Script executed:

cat -n ./composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '170,200p'

Repository: rainxchzed/Github-Store

Length of output: 1334


Call fsync on the actual write stream before it closes; do not reopen the entry.

At line 126, reopening "base.apk" with session.openWrite("base.apk", 0, 0) creates a new stream that is never closed, causing a file descriptor leak. Additionally, fsync must be called on the original stream that contains the written data, not a freshly opened one. Move session.fsync(outputStream) inside the use block after outputStream.flush() (line 121) and remove the incorrect call at line 126.

Suggested fix
             session.openWrite("base.apk", 0, fileSize).use { outputStream ->
                 FileInputStream(apkFile).use { inputStream ->
                     val buffer = ByteArray(65536)
                     var read: Int

                     while (inputStream.read(buffer).also { read = it } != -1) {
                         outputStream.write(buffer, 0, read)
                         totalWritten += read

                         val progress = ((totalWritten.toFloat() / fileSize) * 100).toInt()
                         if (progress >= lastEmittedProgress + 5) {
                             onProgress(SessionProgress.Writing(progress))
                             lastEmittedProgress = progress
                         }
                     }

                     outputStream.flush()
+                    session.fsync(outputStream)
                 }
             }

             onProgress(SessionProgress.Writing(100))
-            session.fsync(session.openWrite("base.apk", 0, 0))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
session.openWrite("base.apk", 0, fileSize).use { outputStream ->
FileInputStream(apkFile).use { inputStream ->
val buffer = ByteArray(65536)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
totalWritten += read
val progress = ((totalWritten.toFloat() / fileSize) * 100).toInt()
if (progress >= lastEmittedProgress + 5) {
onProgress(SessionProgress.Writing(progress))
lastEmittedProgress = progress
}
}
outputStream.flush()
}
}
onProgress(SessionProgress.Writing(100))
session.fsync(session.openWrite("base.apk", 0, 0))
session.openWrite("base.apk", 0, fileSize).use { outputStream ->
FileInputStream(apkFile).use { inputStream ->
val buffer = ByteArray(65536)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
totalWritten += read
val progress = ((totalWritten.toFloat() / fileSize) * 100).toInt()
if (progress >= lastEmittedProgress + 5) {
onProgress(SessionProgress.Writing(progress))
lastEmittedProgress = progress
}
}
outputStream.flush()
session.fsync(outputStream)
}
}
onProgress(SessionProgress.Writing(100))
🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt`
around lines 105 - 127, The code currently reopens the entry with
session.openWrite("base.apk", 0, 0) and calls session.fsync on that new stream
(causing a leaked descriptor) instead of fsync-ing the original write stream;
fix this in ShizukuInstaller.kt by moving the session.fsync call into the
existing session.openWrite(...).use { outputStream -> ... } block right after
outputStream.flush(), calling session.fsync(outputStream) on that same
outputStream, and remove the later session.openWrite("base.apk", 0, 0) /
session.fsync(...) call.

Comment on lines +213 to +239
return suspendCancellableCoroutine { continuation ->
try {
val packageInstaller = context.packageManager.packageInstaller

val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = UNINSTALL_ACTION
putExtra(EXTRA_PACKAGE_NAME, packageName)
}

val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}

val pendingIntent = PendingIntent.getBroadcast(
context,
packageName.hashCode(),
intent,
flags
)

uninstallCallbacks[packageName] = { success, message ->
if (continuation.isActive) {
continuation.resume(UninstallResult(success, message))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -p "ShizukuInstaller.kt" -t f

Repository: rainxchzed/Github-Store

Length of output: 181


🏁 Script executed:

wc -l composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt

Repository: rainxchzed/Github-Store

Length of output: 185


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '200,250p'

Repository: rainxchzed/Github-Store

Length of output: 2294


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | head -100

Repository: rainxchzed/Github-Store

Length of output: 4698


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt | sed -n '250,299p'

Repository: rainxchzed/Github-Store

Length of output: 2211


Remove uninstall callback on cancellation and error paths to prevent leaks.

If the coroutine is cancelled before the broadcast arrives, the callback in uninstallCallbacks remains registered indefinitely. Additionally, if an exception occurs before the normal completion path, the callback is never cleaned up. Add cancellation cleanup and error path cleanup to prevent callbacks from accumulating.

🛠️ Suggested fix
                 uninstallCallbacks[packageName] = { success, message ->
                     if (continuation.isActive) {
                         continuation.resume(UninstallResult(success, message))
                     }
                 }
 
+                continuation.invokeOnCancellation {
+                    uninstallCallbacks.remove(packageName)
+                }
+
                 packageInstaller.uninstall(packageName, pendingIntent.intentSender)
 
             } catch (e: Exception) {
+                uninstallCallbacks.remove(packageName)
                 Logger.e(e) { "Failed to uninstall $packageName" }
                 if (continuation.isActive) {
                     continuation.resume(UninstallResult(false, e.message ?: "Uninstall failed"))
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return suspendCancellableCoroutine { continuation ->
try {
val packageInstaller = context.packageManager.packageInstaller
val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = UNINSTALL_ACTION
putExtra(EXTRA_PACKAGE_NAME, packageName)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getBroadcast(
context,
packageName.hashCode(),
intent,
flags
)
uninstallCallbacks[packageName] = { success, message ->
if (continuation.isActive) {
continuation.resume(UninstallResult(success, message))
}
}
return suspendCancellableCoroutine { continuation ->
try {
val packageInstaller = context.packageManager.packageInstaller
val intent = Intent(context, InstallResultReceiver::class.java).apply {
action = UNINSTALL_ACTION
putExtra(EXTRA_PACKAGE_NAME, packageName)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getBroadcast(
context,
packageName.hashCode(),
intent,
flags
)
uninstallCallbacks[packageName] = { success, message ->
if (continuation.isActive) {
continuation.resume(UninstallResult(success, message))
}
}
continuation.invokeOnCancellation {
uninstallCallbacks.remove(packageName)
}
packageInstaller.uninstall(packageName, pendingIntent.intentSender)
} catch (e: Exception) {
uninstallCallbacks.remove(packageName)
Logger.e(e) { "Failed to uninstall $packageName" }
if (continuation.isActive) {
continuation.resume(UninstallResult(false, e.message ?: "Uninstall failed"))
}
🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.kt`
around lines 213 - 239, The uninstall callback stored in uninstallCallbacks
inside the suspendCancellableCoroutine block can leak if the coroutine is
cancelled or an exception occurs; update the suspendCancellableCoroutine usage
in ShizukuInstaller.kt so that after registering uninstallCallbacks[packageName]
= { ... } you (1) register continuation.invokeOnCancellation {
uninstallCallbacks.remove(packageName) } to remove the callback on cancellation
and (2) wrap the setup in try/catch/finally (or ensure a failure path) that
removes uninstallCallbacks.remove(packageName) when an exception is thrown
before completion, ensuring the callback is always removed in all paths that
exit suspendCancellableCoroutine prematurely.

Comment on lines +43 to +48
init {
Shizuku.addBinderReceivedListenerSticky(binderReceivedListener)
Shizuku.addBinderDeadListener(binderDeadListener)
Shizuku.addRequestPermissionResultListener(permissionResultListener)
checkAvailability()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the ShizukuManager file and read the full context
find . -name "ShizukuManager.kt" -type f

Repository: rainxchzed/Github-Store

Length of output: 181


🏁 Script executed:

# Once found, read the init and cleanup methods with context
cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt

Repository: rainxchzed/Github-Store

Length of output: 5733


🏁 Script executed:

# Search for all cleanup() calls in the codebase
rg -n 'cleanup\(\)' --type=kt -B2 -A2

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

# Search for ShizukuManager usage, especially in DI or lifecycle contexts
rg -n 'ShizukuManager' --type=kt -B2 -A2

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

# Check for any lifecycle-related patterns or interfaces
rg -n 'onDestroy|onDispose|RemovalCallback|LifecycleObserver' --type=kt

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

# Search for cleanup() calls across all Kotlin files
rg 'cleanup\(\)' -B2 -A2

Repository: rainxchzed/Github-Store

Length of output: 745


🏁 Script executed:

# Search for ShizukuManager usage and instantiation
rg 'ShizukuManager' -B2 -A2

Repository: rainxchzed/Github-Store

Length of output: 4597


🏁 Script executed:

# Search for DI module setup - look for di, module, hilt, koin patterns
rg -i 'module|single|factory|inject' | grep -i shizuku

Repository: rainxchzed/Github-Store

Length of output: 524


🏁 Script executed:

# Check if ShizukuManager is used in any lifecycle-aware component
rg 'ShizukuManager' --type=kt

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

# Search for Application class or app lifecycle setup
find . -name "*Application*.kt" -type f | head -20

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

# Search for onDestroy, onDispose or lifecycle patterns
rg 'onDestroy|onDispose|onCleared' -A3 -B3

Repository: rainxchzed/Github-Store

Length of output: 4797


🏁 Script executed:

# Check the full PlatformModules.android.kt to see all DI setup
cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt

Repository: rainxchzed/Github-Store

Length of output: 4617


🏁 Script executed:

# Search for any shutdown or cleanup hooks
rg 'shutdown|unload|destroy|dispose' -i

Repository: rainxchzed/Github-Store

Length of output: 464


Call cleanup() during app shutdown to prevent listener leak.

The init block registers three listeners (binderReceivedListener, binderDeadListener, permissionResultListener) on the Shizuku singleton. Since ShizukuManager is a Koin singleton that lives for the entire app lifetime, cleanup() must be called during app teardown. Currently, cleanup() is never invoked anywhere in the codebase, causing the listeners to remain registered indefinitely. Consider integrating this into an Application lifecycle callback or Koin's onClose callback:

single<ShizukuManager> {
    ShizukuManager(context = get())
}.apply {
    onClose { get<ShizukuManager>().cleanup() }
}
🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt`
around lines 43 - 48, The ShizukuManager registers listeners in its init
(binderReceivedListener, binderDeadListener, permissionResultListener) but never
calls cleanup(), causing a listener leak; update app DI or lifecycle to invoke
ShizukuManager.cleanup() on shutdown—either add an onClose callback to the Koin
single that constructs ShizukuManager (call get<ShizukuManager>().cleanup() in
onClose) or wire cleanup() into your Application lifecycle (e.g., in onTerminate
or a LifecycleObserver) so the listeners are unregistered when the app / Koin
context is closed.

Comment on lines +18 to 21
val installedAssetName: String,
val installedAssetUrl: String,
val latestVersion: String,
val latestAssetName: String?,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt

Repository: rainxchzed/Github-Store

Length of output: 1744


🏁 Script executed:

fd -e kt | xargs rg -l '@Database' | head -5

Repository: rainxchzed/Github-Store

Length of output: 157


🏁 Script executed:

fd -e kt | xargs rg -l 'Migration' | head -10

Repository: rainxchzed/Github-Store

Length of output: 565


🏁 Script executed:

rg -n --type=kotlin 'installed_apps' -C2 | head -30

Repository: rainxchzed/Github-Store

Length of output: 3992


🏁 Script executed:

fd -e kt | xargs rg -l 'InstalledApp.*mapper\|InstalledAppDto' | head -10

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt

Repository: rainxchzed/Github-Store

Length of output: 1397


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/db/migrations/MIGRATION_1_2.kt

Repository: rainxchzed/Github-Store

Length of output: 782


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/db/migrations/MIGRATION_2_3.kt

Repository: rainxchzed/Github-Store

Length of output: 1455


🏁 Script executed:

fd -e kt 'MIGRATION.*\.kt$' | grep -E 'MIGRATION_[0-9]+_[0-9]+' | sort

Repository: rainxchzed/Github-Store

Length of output: 276


🏁 Script executed:

fd -e kt 'MIGRATION.*\.kt$'

Repository: rainxchzed/Github-Store

Length of output: 276


🏁 Script executed:

git log --oneline -n 20 -- 'composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt' 2>/dev/null || echo "Git log not available"

Repository: rainxchzed/Github-Store

Length of output: 144


🏁 Script executed:

git diff HEAD~5..HEAD -- 'composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt' 2>/dev/null | head -80 || echo "Git diff not available"

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

rg -n 'autoUpdateEnabled|autoUpdateFailCount' --type=kotlin -A1 -B1 | head -30

Repository: rainxchzed/Github-Store

Length of output: 2232


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/db/initDatabase.kt

Repository: rainxchzed/Github-Store

Length of output: 981


Add/verify Room migration for new auto-update columns.

New columns autoUpdateEnabled, lastAutoUpdateAttempt, autoUpdateFailCount, and autoUpdateFailReason (lines 39–42) are added to the entity but no corresponding migration to version 4 exists. The database version remains 3, and initDatabase.kt only registers migrations 1→2 and 2→3. Without MIGRATION_3_4, Room schema validation will fail on app upgrade and reading these columns will crash. Add database version bump to 4 and create the migration with ALTER TABLE statements to add the columns (or use @ColumnInfo(defaultValue=...) to ensure non-null columns have SQL defaults).

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt`
around lines 18 - 21, The InstalledApp entity was extended with
autoUpdateEnabled, lastAutoUpdateAttempt, autoUpdateFailCount, and
autoUpdateFailReason but no Room migration to version 4 exists; add/upgrade the
DB version constant to 4 and implement MIGRATION_3_4 (referenced where existing
MIGRATION_1_2 and MIGRATION_2_3 are registered, e.g., initDatabase.kt) that
executes ALTER TABLE ... ADD COLUMN statements for the four new fields (or
annotate the InstalledApp properties with `@ColumnInfo`(defaultValue = "...") and
ensure the SQL defaults match to avoid NOT NULL issues), then register
MIGRATION_3_4 alongside the other migrations so Room schema validation succeeds
on upgrade.

Comment on lines 415 to 449
// Auto-update toggle (only show for installed apps with Shizuku)
if (isInstalled && isShizukuAvailable) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onAction(DetailsAction.ToggleAutoUpdate)
}
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = stringResource(Res.string.show_install_options),
modifier = Modifier.size(24.dp),
tint = if (enabled) {
when {
isUpdateAvailable -> MaterialTheme.colorScheme.onTertiary
isInstalled -> MaterialTheme.colorScheme.onSecondary
else -> MaterialTheme.colorScheme.onPrimary
}
} else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Update,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(Res.string.auto_update),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
}

Switch(
checked = installedApp.autoUpdateEnabled,
onCheckedChange = {
onAction(DetailsAction.ToggleAutoUpdate)
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd SmartInstallButton.kt

Repository: rainxchzed/Github-Store

Length of output: 185


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt | sed -n '410,455p'

Repository: rainxchzed/Github-Store

Length of output: 1991


🏁 Script executed:

head -20 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt

Repository: rainxchzed/Github-Store

Length of output: 1044


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt | head -40

Repository: rainxchzed/Github-Store

Length of output: 2289


🏁 Script executed:

rg "toggleable" composeApp/src/commonMain/kotlin/ --type kotlin

Repository: rainxchzed/Github-Store

Length of output: 393


🏁 Script executed:

rg "from androidx.compose" composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt | head -30

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/components/sections/Appearance.kt | grep -A 5 -B 5 "toggleable"

Repository: rainxchzed/Github-Store

Length of output: 1176


🏁 Script executed:

rg "Role.Switch" composeApp/src/commonMain/kotlin/ --type kotlin

Repository: rainxchzed/Github-Store

Length of output: 227


🏁 Script executed:

rg "androidx.compose.ui.semantics" composeApp/src/commonMain/kotlin/ --type kotlin | head -10

Repository: rainxchzed/Github-Store

Length of output: 229


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/components/sections/Appearance.kt | sed -n '365,390p'

Repository: rainxchzed/Github-Store

Length of output: 1104


🏁 Script executed:

rg "onCheckedChange = null" composeApp/src/commonMain/kotlin/ --type kotlin

Repository: rainxchzed/Github-Store

Length of output: 226


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/components/sections/Appearance.kt | sed -n '390,410p'

Repository: rainxchzed/Github-Store

Length of output: 843


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/components/sections/Appearance.kt | sed -n '380,400p'

Repository: rainxchzed/Github-Store

Length of output: 949


Avoid double‑toggle on auto‑update row.

Both the Row clickable modifier and the Switch onCheckedChange call ToggleAutoUpdate, which can cause the action to fire twice due to event propagation. Replace clickable with Modifier.toggleable and make the Switch read-only. This pattern is already established elsewhere in the codebase (see Appearance.kt).

🔧 Suggested pattern
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .clickable {
-                        onAction(DetailsAction.ToggleAutoUpdate)
-                    }
-                    .padding(horizontal = 16.dp, vertical = 8.dp),
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .toggleable(
+                        value = installedApp.autoUpdateEnabled,
+                        role = Role.Switch,
+                        onValueChange = { onAction(DetailsAction.ToggleAutoUpdate) }
+                    )
+                    .padding(horizontal = 16.dp, vertical = 8.dp),
                 horizontalArrangement = Arrangement.SpaceBetween,
                 verticalAlignment = Alignment.CenterVertically
             ) {
@@
-                Switch(
-                    checked = installedApp.autoUpdateEnabled,
-                    onCheckedChange = {
-                        onAction(DetailsAction.ToggleAutoUpdate)
-                    }
-                )
+                Switch(
+                    checked = installedApp.autoUpdateEnabled,
+                    onCheckedChange = null
+                )
             }

Required imports:

+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.ui.semantics.Role
🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt`
around lines 415 - 449, Replace the Row's Modifier.clickable handler with
Modifier.toggleable(...) so the whole row handles toggling via toggleable(role =
Role.Switch, checked = installedApp.autoUpdateEnabled, onValueChange = {
onAction(DetailsAction.ToggleAutoUpdate) }) and make the Switch read‑only by
removing its onCheckedChange (leave checked = installedApp.autoUpdateEnabled and
no onCheckedChange) so the action is only fired once; update imports to include
androidx.compose.foundation.selection.toggleable and
androidx.compose.ui.semantics.Role if needed and keep references to
DetailsAction.ToggleAutoUpdate, installedApp.autoUpdateEnabled, and
SmartInstallButton.

Comment on lines 517 to 533
DetailsAction.RequestShizukuPermission -> {
val granted = installer.requestShizukuPermission()
if (!granted) {
viewModelScope.launch {
_events.send(
DetailsEvent.OnMessage(
getString(Res.string.shizuku_permission_requested)
)
)
}
} else {
// Permission already granted, update state
_state.value = _state.value.copy(
isShizukuAvailable = installer.isShizukuAvailable()
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd composeApp && find . -type f -name "*.kt" | head -20

Repository: rainxchzed/Github-Store

Length of output: 1912


🏁 Script executed:

# First, let's locate the DetailsViewModel file and examine its structure
find composeApp -name "DetailsViewModel.kt" -type f

Repository: rainxchzed/Github-Store

Length of output: 172


🏁 Script executed:

# Search for the installer definition and its methods
rg "requestShizukuPermission" -A 5 -B 2 --type kotlin

Repository: rainxchzed/Github-Store

Length of output: 4512


🏁 Script executed:

# Search for the installer interface/class definition
rg "class.*[Ii]nstaller|interface.*[Ii]nstaller" -A 10 --type kotlin

Repository: rainxchzed/Github-Store

Length of output: 6056


🏁 Script executed:

# Search for Shizuku-related code
rg "Shizuku" -B 2 -A 5 --type kotlin | head -100

Repository: rainxchzed/Github-Store

Length of output: 13835


🏁 Script executed:

cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt | head -150

Repository: rainxchzed/Github-Store

Length of output: 5733


🏁 Script executed:

# Check for lifecycle management in the ViewModel or Activity
rg "onResume|onPause|onStart|onStop" --type kotlin | head -50

Repository: rainxchzed/Github-Store

Length of output: 2552


🏁 Script executed:

# Look for lifecycle scope usage in DetailsViewModel
rg "lifecycleScope|viewModelScope" composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt -B 3 -A 3

Repository: rainxchzed/Github-Store

Length of output: 4291


🏁 Script executed:

# Check if there's any observation of Shizuku state
rg "isShizukuAvailable|hasPermission" --type kotlin -B 2 -A 2 | head -100

Repository: rainxchzed/Github-Store

Length of output: 12819


🏁 Script executed:

# Search for any StateFlow or Flow observation of shizukuManager in DetailsViewModel
rg "shizukuManager|hasPermission|isAvailable" composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt -B 2 -A 2

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

# Check the complete DetailsViewModel to see the full context of state initialization
wc -l composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt

Repository: rainxchzed/Github-Store

Length of output: 176


🏁 Script executed:

# Look for where isShizukuAvailable is set in the state flow
rg "isShizukuAvailable" composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt -B 5 -A 5

Repository: rainxchzed/Github-Store

Length of output: 1985


Observe Shizuku permission state changes instead of one-time check.

requestShizukuPermission() triggers an async system dialog that returns false immediately, with the actual result delivered via ShizukuManager's callback. The state is only updated in the "already granted" branch, so if the user grants permission through the dialog, the ViewModel never learns about it—the UI remains stale until reload. Collect shizukuManager.hasPermission StateFlow to refresh isShizukuAvailable when permission changes.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`
around lines 517 - 533, The RequestShizukuPermission branch in DetailsViewModel
currently treats installer.requestShizukuPermission() as a synchronous result
and only updates _state when permission was already granted; instead, subscribe
to the shizukuManager.hasPermission StateFlow (or equivalent permission-change
callback) inside the ViewModel (e.g., in init or when handling
DetailsAction.RequestShizukuPermission) using viewModelScope.launch/collect to
update _state.copy(isShizukuAvailable = installer.isShizukuAvailable()) whenever
permission changes, keep sending the OnMessage event for the immediate false
return if you still want to show the dialog fired, and remove relying on the
immediate boolean to reflect the final permission state so the UI refreshes when
the async dialog result arrives.

This commit replaces the direct navigation for Shizuku setup with a new interactive dialog. This dialog guides the user through the process of installing Shizuku, starting the service, and granting the necessary permissions, providing a more user-friendly setup experience.

The status of each step is now polled periodically to provide real-time feedback within the dialog.

Specific changes:
- **feat(shizuku)**: Introduced `ShizukuSetupDialog`, a new composable that displays the step-by-step setup process for Shizuku.
- **feat(shizuku)**: Implemented `RefreshShizukuStatus`, `OpenShizukuApp`, and `OnShizukuRequestPermission` actions to interact with the setup dialog.
- **refactor(shizuku)**: Replaced the `isShizukuAvailable` state with more specific states: `isShizukuInstalled`, `isShizukuRunning`, and `hasShizukuPermission`.
- **refactor(details)**: Changed `OpenShizukuSetup` action to `OpenShizukuSetupDialog` to launch the new modal.
- **refactor(details)**: The `DetailsViewModel` now fetches and manages the detailed Shizuku status.
- **fix(i18n)**: Added new string resources for the Shizuku setup dialog.
Copy link
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)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt (1)

105-115: Guard DB pruning against data loss on non-Android platforms.

DesktopPackageMonitor.getAllInstalledPackageNames() returns an empty set, causing the pruning logic to delete all apps from the database. The app already declares QUERY_ALL_PACKAGES in AndroidManifest.xml, so Android package visibility is properly configured. However, the deletion should be gated behind a platform check to prevent unintended cleanup on desktop.

Suggested fix
 appsInDb.forEach { app ->
-    if (!installedPackageNames.contains(app.packageName)) {
+    if (platform.type == PlatformType.ANDROID && !installedPackageNames.contains(app.packageName)) {
         Logger.d { "App ${app.packageName} no longer installed (not in system packages), removing from DB" }
         installedAppsRepository.deleteInstalledApp(app.packageName)
     } else if (app.installedVersionName == null) {  // Migrate only if new fields unset
🤖 Fix all issues with AI agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt`:
- Line 7: The commonMain import of java.io.File and the method
installWithShizukuProgress(file: File) make Installer.kt JVM-only; replace the
JVM-specific type with a KMP-safe abstraction—prefer using okio.Path: remove the
java.io.File import, change the method signature in Installer
(installWithShizukuProgress) to accept okio.Path and update any default
implementation to throw UnsupportedOperationException as before, then update
platform-specific implementations (actuals) on JVM/Android to convert between
okio.Path and java.io.File when needed; alternatively, if you prefer platform
separation, move installWithShizukuProgress into an Android-only extension or
use an expect/actual wrapper type for file paths and implement the actual on
Android to avoid touching commonMain with java.io.File.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt`:
- Around line 127-145: The ShizukuSetupDialog in DetailsRoot.kt is receiving the
wrong state fields—replace isShizukuInstalled = state.isShizukuEnabled with
isShizukuInstalled = state.isShizukuInstalled and replace isShizukuRunning =
state.isShizukuAvailable with isShizukuRunning = state.isShizukuRunning so the
dialog uses the correct DetailsState fields (isShizukuInstalled,
isShizukuRunning) instead of isShizukuEnabled/isShizukuAvailable when calling
ShizukuSetupDialog and keep the existing action lambdas unchanged.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`:
- Around line 596-610: RefreshShizukuStatus currently assigns both
isShizukuRunning and hasShizukuPermission using installer.isShizukuAvailable(),
conflating running vs permission; change the second call so hasShizukuPermission
uses the correct installer method (the one you introduced/used in the earlier
fix — e.g., installer.hasShizukuPermission() or
installer.isShizukuPermissionGranted()), leaving isShizukuRunning as
installer.isShizukuAvailable(), and update the _state.copy assignments and the
Logger message accordingly to reflect the distinct values.
- Around line 197-200: The ViewModel currently sets both isShizukuRunning and
hasShizukuPermission via installer.isShizukuAvailable(), conflating running vs
permission; change isShizukuRunning to use a binder-alive check (e.g.,
installer.pingBinder() or a new Installer.isShizukuRunning()/pingBinder()
method) and change hasShizukuPermission to use a distinct permission check
(e.g., installer.hasShizukuPermission() or Installer.hasPermission()), updating
the Installer interface and its implementations accordingly; if you cannot
modify Installer, branch on PlatformType.ANDROID and query ShizukuManager's
StateFlow/permission API directly in DetailsViewModel to populate
isShizukuRunning and hasShizukuPermission separately (leave
installer.isShizukuInstalled() as-is).
♻️ Duplicate comments (5)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt (1)

48-52: Bug: get() should be androidContext() for context injection.

The ShizukuManager provider uses context = get() which will cause a runtime exception because no Context binding is registered in the DI graph. All other providers in this module correctly use androidContext().

🐛 Proposed fix
     single<ShizukuManager> {
         ShizukuManager(
-            context = get()
+            context = androidContext()
         )
     }
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (2)

523-539: Permission state may not update after user grants permission via dialog.

installer.requestShizukuPermission() triggers an async system dialog and returns false immediately. The state only updates in the granted branch (line 535-538), so if the user grants permission through the dialog, isShizukuAvailable remains stale until the screen is reloaded. Consider observing ShizukuManager.hasPermission StateFlow to reactively update the UI.


541-562: Add error handling to auto-update toggle.

Repository calls (updateApp, getAppByPackage) can throw exceptions but lack error handling here. Other action handlers in this ViewModel wrap repository operations in try/catch with logging.

Suggested fix
             DetailsAction.ToggleAutoUpdate -> {
                 viewModelScope.launch {
+                    try {
                         val app = _state.value.installedApp ?: return@launch
                         val newValue = !app.autoUpdateEnabled

                         installedAppsRepository.updateApp(
                             app.copy(autoUpdateEnabled = newValue)
                         )

                         val updatedApp = installedAppsRepository.getAppByPackage(app.packageName)
                         _state.value = _state.value.copy(installedApp = updatedApp)

                         _events.send(
                             OnMessage(
                                 getString(
                                     if (newValue) Res.string.auto_update_enabled
                                     else Res.string.auto_update_disabled
                                 )
                             )
                         )
+                    } catch (t: Throwable) {
+                        Logger.e { "Failed to toggle auto-update: ${t.message}" }
+                    }
                 }
             }
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.kt (1)

151-158: Critical: Flow returned by installViaShizuku is never collected.

The install() method calls installViaShizuku(file) which returns a Flow<InstallationProgress>, but this flow is never collected. Since Kotlin flows are cold, the installation will not execute when Shizuku is available and install() is called directly.

Note: The ViewModel correctly uses installWithShizukuProgress() directly for Shizuku installations, so this may not be hit in practice. However, the install() contract is broken.

🐛 Proposed fix

Collect the flow and handle progress/errors:

     override suspend fun install(filePath: String, extOrMime: String) {
         val file = File(filePath)
         if (!file.exists()) {
             throw IllegalStateException("APK file not found: $filePath")
         }

         Logger.d { "Installing APK: $filePath" }

         // Try Shizuku installation first if available
         if (isShizukuAvailable()) {
             Logger.d { "Using Shizuku for installation" }
-            installViaShizuku(file)
+            installWithShizukuProgress(file).collect { progress ->
+                when (progress) {
+                    is InstallationProgress.Error -> throw IllegalStateException(progress.message)
+                    is InstallationProgress.Success -> Logger.d { "Shizuku installation completed: ${progress.packageName}" }
+                    else -> Logger.d { "Shizuku installation progress: $progress" }
+                }
+            }
         } else {
             Logger.d { "Using standard installation" }
             installViaStandard(file)
         }
     }
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt (1)

45-50: Call cleanup() during app shutdown to prevent listener leak.

The init block registers three listeners on the Shizuku singleton. Since ShizukuManager is a Koin singleton that lives for the entire app lifetime, cleanup() must be called during app teardown. Currently, cleanup() is never invoked anywhere in the codebase.

Suggested fix using Koin's onClose callback

In PlatformModules.android.kt:

single<ShizukuManager> {
    ShizukuManager(context = androidContext())
}.onClose { it?.cleanup() }
🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/ShizukuSetupDialog.kt (1)

56-61: Consider using a more lifecycle-aware polling approach.

The infinite while(true) loop works because LaunchedEffect cancels when the composable leaves composition, but consider adding isActive check for clarity and to follow coroutine best practices:

 LaunchedEffect(Unit) {
-    while (true) {
+    while (isActive) {
         delay(2000)
         onRefreshStatus()
     }
 }

This makes the cancellation behavior explicit and is a defensive coding pattern.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsState.kt (1)

57-63: Consider documenting the semantic differences between Shizuku state flags.

There are multiple similar-sounding boolean flags that could cause confusion:

  • isShizukuAvailable vs isShizukuRunning
  • isShizukuEnabled vs isShizukuInstalled

This ambiguity may have already led to incorrect usage in DetailsRoot.kt. Adding KDoc comments or consolidating overlapping states would improve maintainability.

/** Whether the Shizuku app is installed on the device */
val isShizukuInstalled: Boolean = false,
/** Whether the Shizuku service is currently running */
val isShizukuRunning: Boolean = false,
/** Whether Shizuku feature is enabled in app settings */
val isShizukuEnabled: Boolean = false,
/** Whether Shizuku is fully operational (installed + running + permitted) */
val isShizukuAvailable: Boolean = false,
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt (1)

42-44: Inconsistent error handling across Shizuku methods.

installWithShizukuProgress throws UnsupportedOperationException while other Shizuku methods return false. This inconsistency could lead to unexpected crashes if a caller doesn't check isShizukuAvailable() first.

Consider returning an error state via the Flow instead:

 fun installWithShizukuProgress(file: File): Flow<InstallationProgress> {
-    throw UnsupportedOperationException("Shizuku installation not supported on this platform")
+    return kotlinx.coroutines.flow.flowOf(
+        InstallationProgress.Error("Shizuku installation not supported on this platform")
+    )
 }

Alternatively, document that callers must check isShizukuAvailable() before invoking this method.

composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt (1)

105-120: Consider logging swallowed exceptions for debugging.

Static analysis flags swallowed exceptions at lines 109 and 117. While returning false/-1 is acceptable for these cases, adding debug-level logging would help troubleshoot unexpected failures.

♻️ Suggested improvement
     fun isShizukuInstalled(): Boolean {
         return try {
             context.packageManager.getPackageInfo("moe.shizuku.privileged.api", 0)
             true
         } catch (e: PackageManager.NameNotFoundException) {
+            Logger.d { "Shizuku not installed" }
             false
         }
     }

     fun getShizukuVersion(): Int {
         return try {
             Shizuku.getVersion()
         } catch (e: Exception) {
+            Logger.d(e) { "Failed to get Shizuku version" }
             -1
         }
     }

import zed.rainxch.githubstore.core.data.model.InstallationProgress
import zed.rainxch.githubstore.core.domain.model.Architecture
import zed.rainxch.githubstore.core.domain.model.GithubAsset
import java.io.File
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check project structure and build configuration
echo "=== Project structure ==="
git ls-files | grep -E "build\.gradle|build\.gradle\.kts|settings\.gradle" | head -10

echo -e "\n=== Checking for Kotlin Multiplatform configuration ==="
fd -e "gradle" -e "kts" | xargs grep -l "kotlin.*multiplatform\|KotlinMultiplatform" 2>/dev/null | head -5

Repository: rainxchzed/Github-Store

Length of output: 214


🏁 Script executed:

# Read the Installer.kt file to see actual imports and usage
echo "=== Reading Installer.kt ==="
cat -n "composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt"

Repository: rainxchzed/Github-Store

Length of output: 1877


🏁 Script executed:

# Check the project's build.gradle files for target platforms
echo "=== Checking build.gradle configuration ==="
find . -name "build.gradle.kts" -o -name "build.gradle" | head -5 | xargs cat 2>/dev/null | head -100

Repository: rainxchzed/Github-Store

Length of output: 3710


java.io.File in commonMain breaks Kotlin Multiplatform compatibility and prevents expanding to non-JVM targets.

The installWithShizukuProgress(file: File) method uses JVM-specific java.io.File in the common source set. While the project currently targets only Android and JVM (both JVM-compatible), this violates KMP best practices and prevents future support for iOS, JS, or WASM.

Since the method's default implementation already throws UnsupportedOperationException, consider:

  1. Using okio.Path (KMP-compatible)
  2. Using an expect/actual pattern with a platform-agnostic abstraction
  3. Moving the method to an Android-only interface extension
🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.kt`
at line 7, The commonMain import of java.io.File and the method
installWithShizukuProgress(file: File) make Installer.kt JVM-only; replace the
JVM-specific type with a KMP-safe abstraction—prefer using okio.Path: remove the
java.io.File import, change the method signature in Installer
(installWithShizukuProgress) to accept okio.Path and update any default
implementation to throw UnsupportedOperationException as before, then update
platform-specific implementations (actuals) on JVM/Android to convert between
okio.Path and java.io.File when needed; alternatively, if you prefer platform
separation, move installWithShizukuProgress into an Android-only extension or
use an expect/actual wrapper type for file paths and implement the actual on
Android to avoid touching commonMain with java.io.File.

Comment on lines +127 to +145
if (state.showShizukuSetupDialog) {
ShizukuSetupDialog(
isShizukuInstalled = state.isShizukuEnabled,
isShizukuRunning = state.isShizukuAvailable,
hasPermission = state.hasShizukuPermission,
onRequestPermission = {
viewModel.onAction(DetailsAction.OnShizukuRequestPermission)
},
onDismiss = {
viewModel.onAction(DetailsAction.CloseShizukuSetupDialog)
},
onOpenShizukuApp = {
viewModel.onAction(DetailsAction.OpenShizukuApp)
},
onRefreshStatus = {
viewModel.onAction(DetailsAction.RefreshShizukuStatus)
},
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the DetailsState definition to see what fields it has
fd "DetailsState" --type=kt | head -20

Repository: rainxchzed/Github-Store

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Search for DetailsState in the kotlin files
rg "data class DetailsState|class DetailsState" --type=kt -A 15

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find and examine ShizukuSetupDialog definition
fd "ShizukuSetupDialog" --type=kt | head -20

Repository: rainxchzed/Github-Store

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Search for ShizukuSetupDialog function/class definition
rg "@Composable\s+fun ShizukuSetupDialog|fun ShizukuSetupDialog" --type=kt -A 15

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find DetailsState in kotlin files
rg "data class DetailsState|class DetailsState" -A 20

Repository: rainxchzed/Github-Store

Length of output: 2927


🏁 Script executed:

#!/bin/bash
# Find ShizukuSetupDialog definition
rg "@Composable.*fun ShizukuSetupDialog|fun ShizukuSetupDialog" -A 15

Repository: rainxchzed/Github-Store

Length of output: 2341


🏁 Script executed:

#!/bin/bash
# Check the actual DetailsRoot.kt file around lines 127-145
cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt | sed -n '120,150p'

Repository: rainxchzed/Github-Store

Length of output: 1250


🏁 Script executed:

#!/bin/bash
# Find all references to isShizukuInstalled, isShizukuEnabled, isShizukuAvailable, isShizukuRunning
rg "isShizukuInstalled|isShizukuEnabled|isShizukuAvailable|isShizukuRunning" -B 2 -A 2

Repository: rainxchzed/Github-Store

Length of output: 29150


Fix incorrect state field mapping in ShizukuSetupDialog call.

The prop mapping is incorrect:

  • isShizukuInstalled = state.isShizukuEnabled should be state.isShizukuInstalled
  • isShizukuRunning = state.isShizukuAvailable should be state.isShizukuRunning

DetailsState has distinct fields for each status (isShizukuEnabled controls feature availability, while isShizukuInstalled and isShizukuRunning track actual Shizuku states). The dialog uses these parameters to determine step completion, so passing the wrong values causes incorrect UI state and prevents the setup dialog from properly reflecting actual Shizuku setup progress.

Proposed fix
    if (state.showShizukuSetupDialog) {
        ShizukuSetupDialog(
-           isShizukuInstalled = state.isShizukuEnabled,
-           isShizukuRunning = state.isShizukuAvailable,
+           isShizukuInstalled = state.isShizukuInstalled,
+           isShizukuRunning = state.isShizukuRunning,
            hasPermission = state.hasShizukuPermission,
🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt`
around lines 127 - 145, The ShizukuSetupDialog in DetailsRoot.kt is receiving
the wrong state fields—replace isShizukuInstalled = state.isShizukuEnabled with
isShizukuInstalled = state.isShizukuInstalled and replace isShizukuRunning =
state.isShizukuAvailable with isShizukuRunning = state.isShizukuRunning so the
dialog uses the correct DetailsState fields (isShizukuInstalled,
isShizukuRunning) instead of isShizukuEnabled/isShizukuAvailable when calling
ShizukuSetupDialog and keep the existing action lambdas unchanged.

Comment on lines 197 to 200
val isShizukuEnabled = platform.type == PlatformType.ANDROID
val isShizukuInstalled = installer.isShizukuInstalled()
val isShizukuRunning = installer.isShizukuAvailable()
val hasShizukuPermission = installer.isShizukuAvailable()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Semantic mismatch: isShizukuRunning and hasShizukuPermission use the same check.

Both isShizukuRunning and hasShizukuPermission call installer.isShizukuAvailable(), which returns true only when Shizuku is both running AND has permission. This conflates two distinct states:

  • isShizukuRunning should reflect whether Shizuku's binder is alive (i.e., pingBinder() succeeds).
  • hasShizukuPermission should reflect whether the app has been granted Shizuku permission.

The UI may incorrectly show "not running" when Shizuku is actually running but permission is missing.

🐛 Proposed fix

Consider exposing separate methods in Installer interface or use ShizukuManager's distinct StateFlows:

-                val isShizukuRunning = installer.isShizukuAvailable()
-                val hasShizukuPermission = installer.isShizukuAvailable()
+                val isShizukuRunning = installer.isShizukuRunning()  // New method: just checks pingBinder
+                val hasShizukuPermission = installer.hasShizukuPermission()  // New method: just checks permission

Alternatively, if the Installer interface cannot be changed, the ViewModel could directly access ShizukuManager on Android.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`
around lines 197 - 200, The ViewModel currently sets both isShizukuRunning and
hasShizukuPermission via installer.isShizukuAvailable(), conflating running vs
permission; change isShizukuRunning to use a binder-alive check (e.g.,
installer.pingBinder() or a new Installer.isShizukuRunning()/pingBinder()
method) and change hasShizukuPermission to use a distinct permission check
(e.g., installer.hasShizukuPermission() or Installer.hasPermission()), updating
the Installer interface and its implementations accordingly; if you cannot
modify Installer, branch on PlatformType.ANDROID and query ShizukuManager's
StateFlow/permission API directly in DetailsViewModel to populate
isShizukuRunning and hasShizukuPermission separately (leave
installer.isShizukuInstalled() as-is).

…ture

This commit refactors the Shizuku integration for more robust status checking and removes the per-app auto-update toggle, which is not yet implemented. It also improves user feedback during installation and permission requests.

- **Refactor(Shizuku)**: Replaced the `isShizukuAvailable` state property with a computed property that accurately reflects Shizuku's status (enabled, installed, running, and permission granted).
- **Refactor(Shizuku)**: Implemented safer, sequential checks for Shizuku's status (installed -> running -> permission) to prevent crashes.
- **Refactor(Shizuku)**: Added exception handling and user feedback messages for permission requests and installation failures.
- **Chore(Auto-update)**: Removed the auto-update toggle UI and related logic, as the feature is not yet functional. This simplifies the `InstalledApp` entity and the details screen.
- **i18n**: Added new string resources for installation success/failure and Shizuku permission status.
Copy link
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: 5

🤖 Fix all issues with AI agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt`:
- Around line 11-13: Update the DI binding in PlatformModules.android.kt for
single<ShizukuManager> to pass the Android app context using androidContext()
instead of get() (i.e., construct ShizukuManager(context = androidContext()));
also modify ShizukuManager to retain the application context (e.g., store
context.applicationContext in its constructor or an internal property) to avoid
leaking an Activity/Component context since the binding is a singleton.

In `@composeApp/src/commonMain/composeResources/values/strings.xml`:
- Around line 338-347: The listed string resources (setup_shizuku,
shizuku_setup_description, install_shizuku_app, download_shizuku,
start_shizuku_service, open_shizuku, grant_permission_to_github_store,
grant_permission, tap_start_in_shizuku, shizuku_setup_complete) currently use
placeholder key names as values and will show raw keys to users; replace each
value with a proper user-facing sentence or short label (e.g., "Setup Shizuku",
"Follow the steps below to configure Shizuku", "Install the Shizuku app", etc.)
in the strings.xml entry for each corresponding name, and ensure corresponding
localized strings are added/updated in other locale resource files (keep the
same resource names: setup_shizuku, shizuku_setup_description, etc.) so users
see meaningful text instead of keys.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt`:
- Around line 13-21: Add an explicit Room Migration that updates the
InstalledApp table schema to match the changed nullability in InstalledApp
(entity) by recreating the table and copying data with COALESCE for columns that
became non-nullable; implement a Migration object (e.g., val MIGRATION_X_Y) that
runs SQL: create a new table with the new NOT NULL constraints, insert into the
new table selecting from the old table but wrap values for installedAssetName,
installedAssetUrl and latestVersion with COALESCE(column, '') or appropriate
default, drop the old table and rename the new table to the original name, then
register this Migration in your Room database builder via addMigrations(...).
Ensure you reference the InstalledApp entity column names: repoOwnerAvatarUrl,
installedAssetName, installedAssetUrl, latestVersion when writing the SQL.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt`:
- Around line 102-180: The button label and click handler can diverge because
buttonText treats an installedApp.installedVersion !=
state.latestRelease?.tagName as an update while the click uses
isUpdateAvailable; create a single shared predicate (e.g., shouldUpdate or
isUpdateIntent) that encapsulates the update condition (referencing
installedApp, installedApp.installedVersion, state.latestRelease?.tagName and/or
isUpdateAvailable) and then use that predicate both when computing buttonText
and in the click handler to choose between DetailsAction.UpdateApp and
DetailsAction.InstallPrimary so the label and action always match.

In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`:
- Around line 791-806: The current path calls saveInstalledAppToDatabase(...)
before calling installer.install(...), which can leave isPendingInstall=true if
install fails; change the flow so that for the non-Shizuku path you either (A)
perform installer.install(filePath, ext) first and only call
saveInstalledAppToDatabase(...) after the install completes successfully, or (B)
wrap installer.install(...) in a try/catch and on any failure call
updatePendingStatus(...) (or an equivalent DB update) to clear isPendingInstall
and avoid stale records; locate the logic around saveInstalledAppToDatabase,
installer.install, and updatePendingStatus in DetailsViewModel to implement one
of these fixes.
♻️ Duplicate comments (3)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt (1)

39-41: Ensure new auto-update columns are covered by a migration.

lastAutoUpdateAttempt, autoUpdateFailCount, and autoUpdateFailReason are new columns; a version bump plus ALTER TABLE/table rebuild migration is required to keep upgrades working.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (2)

640-665: Same semantic conflation in refreshShizukuStatusInternal.

Lines 644-650 repeat the same issue where isShizukuRunning and hasShizukuPermission both use installer.isShizukuAvailable(). This should be fixed alongside the initial load at lines 216-227.


216-227: isShizukuRunning and hasShizukuPermission still use the same check.

Both variables call installer.isShizukuAvailable(), which conflates "service running" with "permission granted". This was flagged in previous reviews but persists:

val isShizukuRunning = if (isShizukuInstalled) {
    installer.isShizukuAvailable()  // Same call
} else false

val hasShizukuPermission = if (isShizukuRunning) {
    installer.isShizukuAvailable()  // Same call
} else false

The UI may incorrectly show "not running" when Shizuku is running but permission is missing. Consider exposing separate methods like installer.isShizukuRunning() and installer.hasShizukuPermission().

🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt (1)

25-32: Consider consolidating RequestShizukuPermission and OnShizukuRequestPermission.

Both actions appear to trigger Shizuku permission requests with nearly identical handling in the ViewModel. If the distinction is intentional (e.g., one for menu-triggered requests, one for dialog callbacks), consider renaming for clarity. Otherwise, consolidate into a single action.

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt (2)

551-619: Consider extracting shared permission request logic.

RequestShizukuPermission (lines 551-580) and OnShizukuRequestPermission (lines 599-619) share nearly identical logic. The only difference is message handling. Consider extracting a private helper:

Suggested refactor
private suspend fun handleShizukuPermissionRequest(showMessages: Boolean) {
    try {
        val granted = installer.requestShizukuPermission()
        if (granted) {
            _state.update {
                it.copy(hasShizukuPermission = true, isShizukuRunning = true)
            }
            if (showMessages) {
                _events.send(OnMessage(getString(Res.string.shizuku_permission_granted)))
            }
        } else {
            if (showMessages) {
                _events.send(OnMessage(getString(Res.string.shizuku_permission_requested)))
            }
            delay(if (showMessages) 500 else 1000)
            refreshShizukuStatusInternal()
        }
    } catch (e: Exception) {
        Logger.e(e) { "Error requesting Shizuku permission" }
        if (showMessages) {
            _events.send(OnMessage(getString(Res.string.shizuku_permission_error)))
        }
    }
}

838-844: String concatenation breaks i18n support.

The error message concatenates a localized string with raw exception text:

getString(Res.string.installation_failed) + ": ${t.message}"

This produces inconsistent output like "Installatie mislukt: English error message" in non-English locales. Consider using a parameterized string resource.

Suggested fix

In strings.xml:

<string name="installation_failed_with_message">Installation failed: %1$s</string>

In ViewModel:

-                        OnMessage(
-                            getString(Res.string.installation_failed) + ": ${t.message}"
-                        )
+                        OnMessage(
+                            getString(Res.string.installation_failed_with_message, t.message ?: "Unknown error")
+                        )
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt (1)

89-94: Disable clicks while an active download is showing.

enabled is derived from isDownloading/isInstalling, while isActiveDownload is derived from state. If they ever differ, the card can appear clickable (ripple) during an active download even though the onClick is effectively a no-op. Consider folding isActiveDownload into the enabled predicate and its remember keys.

🔧 Suggested tweak
-    val enabled = remember(primaryAsset, isDownloading, isInstalling) {
-        primaryAsset != null && !isDownloading && !isInstalling
-    }
-
-    val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE
+    val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE
+    val enabled = remember(primaryAsset, isDownloading, isInstalling, isActiveDownload) {
+        primaryAsset != null && !isDownloading && !isInstalling && !isActiveDownload
+    }

Comment on lines +11 to +13
class ShizukuManager(
private val context: Context
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate ShizukuManager construction sites and inspect the Context passed in
rg -n --type=kt -C3 'ShizukuManager\s*\('

Repository: rainxchzed/Github-Store

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for ShizukuManager construction in Kotlin files
rg -n 'ShizukuManager\s*\(' --include='*.kt' -C3

Repository: rainxchzed/Github-Store

Length of output: 508


🏁 Script executed:

#!/bin/bash
# First, find all Kotlin files, then search for ShizukuManager construction
fd -e kt -x rg -l 'ShizukuManager' {} \;

Repository: rainxchzed/Github-Store

Length of output: 501


🏁 Script executed:

#!/bin/bash
# Check the DI setup for ShizukuManager
cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt

Repository: rainxchzed/Github-Store

Length of output: 4492


🏁 Script executed:

#!/bin/bash
# Read the ShizukuManager class to see context usage
cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt

Repository: rainxchzed/Github-Store

Length of output: 6063


🏁 Script executed:

#!/bin/bash
# Search for Context binding or other DI configuration
rg -n 'bindSingleton|bind<Context|single<Context|factory<Context' composeApp/src/androidMain/

Repository: rainxchzed/Github-Store

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Find all DI/module related files
fd -e kt -path '*di*' -o -path '*module*' | grep -i 'di\|module'

Repository: rainxchzed/Github-Store

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Find DI module files
find composeApp/src/androidMain -name '*Module*' -o -name '*di*' | head -20

Repository: rainxchzed/Github-Store

Length of output: 223


Fix DI setup: ShizukuManager must use androidContext() instead of get().

Line 50 in PlatformModules.android.kt uses context = get(), which has no definition and will fail at runtime. All other services in the module explicitly use context = androidContext(). Update to match the pattern:

single<ShizukuManager> {
    ShizukuManager(
        context = androidContext()
    )
}

Additionally, since this is a single (singleton), consider storing applicationContext internally to prevent potential memory leaks if context changes:

♻️ Defensive implementation
-class ShizukuManager(
-    private val context: Context
-) {
+class ShizukuManager(
+    context: Context
+) {
+    private val appContext: Context = context.applicationContext
-            context.packageManager.getPackageInfo("moe.shizuku.privileged.api", 0)
+            appContext.packageManager.getPackageInfo("moe.shizuku.privileged.api", 0)
🤖 Prompt for AI Agents
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt`
around lines 11 - 13, Update the DI binding in PlatformModules.android.kt for
single<ShizukuManager> to pass the Android app context using androidContext()
instead of get() (i.e., construct ShizukuManager(context = androidContext()));
also modify ShizukuManager to retain the application context (e.g., store
context.applicationContext in its constructor or an internal property) to avoid
leaking an Activity/Component context since the binding is a singleton.

Comment on lines +338 to +347
<string name="setup_shizuku">setup_shizuku</string>
<string name="shizuku_setup_description">shizuku_setup_description</string>
<string name="install_shizuku_app">install_shizuku_app</string>
<string name="download_shizuku">download_shizuku</string>
<string name="start_shizuku_service">start_shizuku_service</string>
<string name="open_shizuku">open_shizuku</string>
<string name="grant_permission_to_github_store">grant_permission_to_github_store</string>
<string name="grant_permission">grant_permission</string>
<string name="tap_start_in_shizuku">tap_start_in_shizuku</string>
<string name="shizuku_setup_complete">shizuku_setup_complete</string>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Placeholder string values will display as raw keys to users.

These strings use the key name as the value instead of actual user-facing text:

<string name="setup_shizuku">setup_shizuku</string>
<string name="shizuku_setup_description">shizuku_setup_description</string>
...

This will result in users seeing "setup_shizuku" instead of meaningful text like "Setup Shizuku".

Suggested values
-    <string name="setup_shizuku">setup_shizuku</string>
-    <string name="shizuku_setup_description">shizuku_setup_description</string>
-    <string name="install_shizuku_app">install_shizuku_app</string>
-    <string name="download_shizuku">download_shizuku</string>
-    <string name="start_shizuku_service">start_shizuku_service</string>
-    <string name="open_shizuku">open_shizuku</string>
-    <string name="grant_permission_to_github_store">grant_permission_to_github_store</string>
-    <string name="grant_permission">grant_permission</string>
-    <string name="tap_start_in_shizuku">tap_start_in_shizuku</string>
-    <string name="shizuku_setup_complete">shizuku_setup_complete</string>
+    <string name="setup_shizuku">Setup Shizuku</string>
+    <string name="shizuku_setup_description">Shizuku enables silent app installation without prompts. Follow these steps to set it up:</string>
+    <string name="install_shizuku_app">Install Shizuku app</string>
+    <string name="download_shizuku">Download Shizuku</string>
+    <string name="start_shizuku_service">Start Shizuku service</string>
+    <string name="open_shizuku">Open Shizuku</string>
+    <string name="grant_permission_to_github_store">Grant permission to GitHub Store</string>
+    <string name="grant_permission">Grant Permission</string>
+    <string name="tap_start_in_shizuku">Tap "Start" in Shizuku app</string>
+    <string name="shizuku_setup_complete">Shizuku setup complete!</string>
🤖 Prompt for AI Agents
In `@composeApp/src/commonMain/composeResources/values/strings.xml` around lines
338 - 347, The listed string resources (setup_shizuku,
shizuku_setup_description, install_shizuku_app, download_shizuku,
start_shizuku_service, open_shizuku, grant_permission_to_github_store,
grant_permission, tap_start_in_shizuku, shizuku_setup_complete) currently use
placeholder key names as values and will show raw keys to users; replace each
value with a proper user-facing sentence or short label (e.g., "Setup Shizuku",
"Follow the steps below to configure Shizuku", "Install the Shizuku app", etc.)
in the strings.xml entry for each corresponding name, and ensure corresponding
localized strings are added/updated in other locale resource files (keep the
same resource names: setup_shizuku, shizuku_setup_description, etc.) so users
see meaningful text instead of keys.

Comment on lines +13 to 21
val repoOwnerAvatarUrl: String?,
val repoDescription: String?,
val primaryLanguage: String?,
val repoUrl: String,
val installedVersion: String,
val installedAssetName: String?,
val installedAssetUrl: String?,
val latestVersion: String?,
val installedAssetName: String,
val installedAssetUrl: String,
val latestVersion: String,
val latestAssetName: String?,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add an explicit Room migration for the schema changes in this PR.

The nullability changes to repoOwnerAvatarUrl (made nullable), installedAssetName, installedAssetUrl, and latestVersion (made non-nullable), plus the changes at line 38, modify the database NOT NULL constraints. Room's schema validation will fail on app upgrade if no migration is provided. Add an explicit migration that recreates the table and uses COALESCE() to safely handle existing NULL values when converting columns from nullable to non-nullable.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.kt`
around lines 13 - 21, Add an explicit Room Migration that updates the
InstalledApp table schema to match the changed nullability in InstalledApp
(entity) by recreating the table and copying data with COALESCE for columns that
became non-nullable; implement a Migration object (e.g., val MIGRATION_X_Y) that
runs SQL: create a new table with the new NOT NULL constraints, insert into the
new table selecting from the old table but wrap values for installedAssetName,
installedAssetUrl and latestVersion with COALESCE(column, '') or appropriate
default, drop the old table and rename the new table to the original name, then
register this Migration in your Room database builder via addMigrations(...).
Ensure you reference the InstalledApp entity column names: repoOwnerAvatarUrl,
installedAssetName, installedAssetUrl, latestVersion when writing the SQL.

Comment on lines 102 to +180
val buttonText = when {
!enabled && primaryAsset == null -> stringResource(Res.string.not_available)
installedApp != null && installedApp.installedVersion != state.latestRelease?.tagName -> stringResource(
Res.string.update_app
)
installedApp != null && installedApp.installedVersion != state.latestRelease?.tagName -> {
if (isShizukuAvailable) {
stringResource(Res.string.silent_update)
} else {
stringResource(Res.string.update_app)
}
}

isUpdateAvailable -> stringResource(
Res.string.update_to_version,
installedApp.latestVersion.toString()
installedApp.latestVersion
)

isInstalled -> stringResource(Res.string.reinstall)
else -> stringResource(Res.string.install_latest)
isInstalled -> {
if (isShizukuAvailable) {
stringResource(Res.string.silent_reinstall)
} else {
stringResource(Res.string.reinstall)
}
}

else -> {
if (isShizukuAvailable) {
stringResource(Res.string.silent_install)
} else {
stringResource(Res.string.install_latest)
}
}
}

Row(
Column(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ElevatedCard(
modifier = Modifier
.weight(1f)
.height(52.dp)
.background(
color = buttonColor,
shape = CircleShape
)
.clickable(
enabled = enabled,
onClick = {
if (!state.isDownloading && state.downloadStage == DownloadStage.IDLE) {
if (isUpdateAvailable) {
onAction(DetailsAction.UpdateApp)
} else {
onAction(DetailsAction.InstallPrimary)
}
}
}
if (isShizukuAvailable && !isActiveDownload) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
.liquefiable(liquidState),
colors = CardDefaults.elevatedCardColors(
containerColor = buttonColor
),
shape = if (state.isObtainiumEnabled || isActiveDownload) {
RoundedCornerShape(
topStart = 24.dp,
bottomStart = 24.dp,
topEnd = 6.dp,
bottomEnd = 6.dp
Spacer(Modifier.width(4.dp))
Text(
text = stringResource(Res.string.shizuku_enabled),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)
} else CircleShape
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (isActiveDownload) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when (state.downloadStage) {
DownloadStage.DOWNLOADING -> {
Text(
text = if (isUpdateAvailable) stringResource(Res.string.updating) else stringResource(
Res.string.downloading
),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold
)

Text(
text = "${progress ?: 0}%",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
)
}

DownloadStage.VERIFYING -> {
Text(
text = stringResource(Res.string.verifying),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold
)
}
}
}

DownloadStage.INSTALLING -> {
Text(
text = if (isUpdateAvailable) stringResource(Res.string.updating) else stringResource(
Res.string.installing
),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
ElevatedCard(
modifier = Modifier
.weight(1f)
.height(52.dp)
.background(
color = buttonColor,
shape = CircleShape
)
.clickable(
enabled = enabled,
onClick = {
if (!state.isDownloading && state.downloadStage == DownloadStage.IDLE) {
if (isUpdateAvailable) {
onAction(DetailsAction.UpdateApp)
} else {
onAction(DetailsAction.InstallPrimary)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align update label with click action predicate.

buttonText treats an installedVersion/latestRelease mismatch as an update, but the click path only checks isUpdateAvailable. If those diverge, the UI can show “Update” while the click routes to InstallPrimary. Consider reusing a shared predicate for both label and action.

🔧 Suggested adjustment
+    val shouldUpdate = isUpdateAvailable ||
+        (installedApp != null && installedApp.installedVersion != state.latestRelease?.tagName)
...
-                                if (isUpdateAvailable) {
+                                if (shouldUpdate) {
                                     onAction(DetailsAction.UpdateApp)
                                 } else {
                                     onAction(DetailsAction.InstallPrimary)
                                 }
🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.kt`
around lines 102 - 180, The button label and click handler can diverge because
buttonText treats an installedApp.installedVersion !=
state.latestRelease?.tagName as an update while the click uses
isUpdateAvailable; create a single shared predicate (e.g., shouldUpdate or
isUpdateIntent) that encapsulates the update condition (referencing
installedApp, installedApp.installedVersion, state.latestRelease?.tagName and/or
isUpdateAvailable) and then use that predicate both when computing buttonText
and in the click handler to choose between DetailsAction.UpdateApp and
DetailsAction.InstallPrimary so the label and action always match.

Comment on lines +791 to +806
if (platform.type == PlatformType.ANDROID) {
saveInstalledAppToDatabase(
assetName = assetName,
assetUrl = downloadUrl,
assetSize = sizeBytes,
releaseTag = releaseTag,
isUpdate = isUpdate,
filePath = filePath
)
} else {
viewModelScope.launch {
_events.send(OnMessage(getString(Res.string.installer_saved_downloads)))
}
}

_state.value = _state.value.copy(downloadStage = DownloadStage.IDLE)
currentAssetName = null
appendLog(
assetName = assetName,
size = sizeBytes,
tag = releaseTag,
result = if (isUpdate) {
LogResult.Updated
} else LogResult.Installed
installer.install(filePath, ext)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "isPendingInstall" --type kotlin -A 3 -B 3

Repository: rainxchzed/Github-Store

Length of output: 4474


Standard install saves to DB before completion, but sync recovery is in place.

In the non-Shizuku path, saveInstalledAppToDatabase is called at line 792 before installer.install() at line 806, creating a window where the database records a pending install before the installation actually completes. If the install fails (e.g., user cancels, app crashes), a stale record with isPendingInstall = true will persist.

However, this is mitigated by sync logic at lines 188-191, which detects completed installations during the next sync: if isPendingInstall is true but the package is confirmed installed, the flag is cleared via updatePendingStatus(). This recovery mechanism works well for normal app lifecycle flows but leaves stale records if the user never syncs again after a failed install.

Consider aligning the standard path's behavior with the Shizuku path by saving to DB only after a successful install confirmation, or ensure the isPendingInstall flag is explicitly reset on install failure within the error handler.

🤖 Prompt for AI Agents
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt`
around lines 791 - 806, The current path calls saveInstalledAppToDatabase(...)
before calling installer.install(...), which can leave isPendingInstall=true if
install fails; change the flow so that for the non-Shizuku path you either (A)
perform installer.install(filePath, ext) first and only call
saveInstalledAppToDatabase(...) after the install completes successfully, or (B)
wrap installer.install(...) in a try/catch and on any failure call
updatePendingStatus(...) (or an equivalent DB update) to clear isPendingInstall
and avoid stale records; locate the logic around saveInstalledAppToDatabase,
installer.install, and updatePendingStatus in DetailsViewModel to implement one
of these fixes.

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