-
-
Notifications
You must be signed in to change notification settings - Fork 147
feat(android): Add support for silent installs via Shizuku #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
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.
There was a problem hiding this 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: WireOnNavigateToShizukuSetupto 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:isShizukuEnabledhas misleading semantics—it should reflect actual permission state or be renamed.Currently set to
platform.type == PlatformType.ANDROID, which is alwaystrueon 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. Sinceinstaller.isShizukuAvailable()already provides the real state (requires bothisAvailable.value && hasPermission.value), either use that directly forisShizukuEnabledor rename toisShizukuSupportedOnPlatformto 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 missingsessionIdbefore forwarding results.If the intent is missing the session ID, passing
-1downstream 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
installWithShizukuProgresswithout gating, this will throw. Please verify all call sites checkisShizukuAvailable/isShizukuInstalled, or consider returning a failureFlowinstead.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.
installViaShizukusimply delegates toinstallWithShizukuProgresswithout adding any behavior. After fixing the flow collection issue ininstall(), 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 ininstall().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
-1is 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
📒 Files selected for processing (20)
composeApp/build.gradle.ktscomposeApp/src/androidMain/AndroidManifest.xmlcomposeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.ktcomposeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/AndroidInstaller.ktcomposeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/InstallResultReceiver.ktcomposeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.ktcomposeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.ktcomposeApp/src/commonMain/composeResources/values/strings.xmlcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/model/InstallationProgress.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/services/Installer.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsState.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/SmartInstallButton.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/components/sections/Header.ktcomposeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.ktgradle/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
AndroidInstallerfor 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 withexported=falsereceiver.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
falsefor availability checks and throwingUnsupportedOperationExceptionfor the installation flow. This maintains API parity with the commonInstallerinterface 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
objectfor stateless states anddata classfor stateful ones is idiomatic Kotlin. This design enables type-safewhenexpressions 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
installWithShizukuProgressis exhaustive, anduninstallWithShizukuproperly guards against unavailability.composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuManager.kt (2)
103-110: Appropriate exception handling.The
NameNotFoundExceptionis expected when Shizuku isn't installed. Returningfalseis the correct behavior here. The static analysis warning is a false positive in this context.
11-71: LGTM! Well-structured Shizuku lifecycle management.The
ShizukuManagerclass provides a clean reactive API withStateFlowfor 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.
| single<ShizukuManager> { | ||
| ShizukuManager( | ||
| context = get() | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // Try Shizuku installation first if available | ||
| if (isShizukuAvailable()) { | ||
| Logger.d { "Using Shizuku for installation" } | ||
| installViaShizuku(file) | ||
| } else { | ||
| Logger.d { "Using standard installation" } | ||
| installViaStandard(file) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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)) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "ShizukuInstaller.kt" -type fRepository: 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 2Repository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
rg "session\.(openWrite|fsync)" -B 2 -A 2Repository: 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 -50Repository: 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.
| 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.
| 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)) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd -p "ShizukuInstaller.kt" -t fRepository: rainxchzed/Github-Store
Length of output: 181
🏁 Script executed:
wc -l composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/installer/shizuku/ShizukuInstaller.ktRepository: 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 -100Repository: 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.
| 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.
| init { | ||
| Shizuku.addBinderReceivedListenerSticky(binderReceivedListener) | ||
| Shizuku.addBinderDeadListener(binderDeadListener) | ||
| Shizuku.addRequestPermissionResultListener(permissionResultListener) | ||
| checkAvailability() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, find the ShizukuManager file and read the full context
find . -name "ShizukuManager.kt" -type fRepository: 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.ktRepository: rainxchzed/Github-Store
Length of output: 5733
🏁 Script executed:
# Search for all cleanup() calls in the codebase
rg -n 'cleanup\(\)' --type=kt -B2 -A2Repository: 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 -A2Repository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
# Check for any lifecycle-related patterns or interfaces
rg -n 'onDestroy|onDispose|RemovalCallback|LifecycleObserver' --type=ktRepository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
# Search for cleanup() calls across all Kotlin files
rg 'cleanup\(\)' -B2 -A2Repository: rainxchzed/Github-Store
Length of output: 745
🏁 Script executed:
# Search for ShizukuManager usage and instantiation
rg 'ShizukuManager' -B2 -A2Repository: 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 shizukuRepository: rainxchzed/Github-Store
Length of output: 524
🏁 Script executed:
# Check if ShizukuManager is used in any lifecycle-aware component
rg 'ShizukuManager' --type=ktRepository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
# Search for Application class or app lifecycle setup
find . -name "*Application*.kt" -type f | head -20Repository: rainxchzed/Github-Store
Length of output: 49
🏁 Script executed:
# Search for onDestroy, onDispose or lifecycle patterns
rg 'onDestroy|onDispose|onCleared' -A3 -B3Repository: 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.ktRepository: rainxchzed/Github-Store
Length of output: 4617
🏁 Script executed:
# Search for any shutdown or cleanup hooks
rg 'shutdown|unload|destroy|dispose' -iRepository: 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.
| val installedAssetName: String, | ||
| val installedAssetUrl: String, | ||
| val latestVersion: String, | ||
| val latestAssetName: String?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/InstalledApp.ktRepository: rainxchzed/Github-Store
Length of output: 1744
🏁 Script executed:
fd -e kt | xargs rg -l '@Database' | head -5Repository: rainxchzed/Github-Store
Length of output: 157
🏁 Script executed:
fd -e kt | xargs rg -l 'Migration' | head -10Repository: rainxchzed/Github-Store
Length of output: 565
🏁 Script executed:
rg -n --type=kotlin 'installed_apps' -C2 | head -30Repository: rainxchzed/Github-Store
Length of output: 3992
🏁 Script executed:
fd -e kt | xargs rg -l 'InstalledApp.*mapper\|InstalledAppDto' | head -10Repository: rainxchzed/Github-Store
Length of output: 49
🏁 Script executed:
cat -n composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.ktRepository: 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.ktRepository: 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.ktRepository: rainxchzed/Github-Store
Length of output: 1455
🏁 Script executed:
fd -e kt 'MIGRATION.*\.kt$' | grep -E 'MIGRATION_[0-9]+_[0-9]+' | sortRepository: 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 -30Repository: rainxchzed/Github-Store
Length of output: 2232
🏁 Script executed:
cat -n composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/local/db/initDatabase.ktRepository: 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.
| // 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) | ||
| } | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd SmartInstallButton.ktRepository: 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.ktRepository: 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 -40Repository: rainxchzed/Github-Store
Length of output: 2289
🏁 Script executed:
rg "toggleable" composeApp/src/commonMain/kotlin/ --type kotlinRepository: 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 -30Repository: 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 kotlinRepository: rainxchzed/Github-Store
Length of output: 227
🏁 Script executed:
rg "androidx.compose.ui.semantics" composeApp/src/commonMain/kotlin/ --type kotlin | head -10Repository: 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 kotlinRepository: 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.
| 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() | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd composeApp && find . -type f -name "*.kt" | head -20Repository: 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 fRepository: rainxchzed/Github-Store
Length of output: 172
🏁 Script executed:
# Search for the installer definition and its methods
rg "requestShizukuPermission" -A 5 -B 2 --type kotlinRepository: 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 kotlinRepository: rainxchzed/Github-Store
Length of output: 6056
🏁 Script executed:
# Search for Shizuku-related code
rg "Shizuku" -B 2 -A 5 --type kotlin | head -100Repository: 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 -150Repository: 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 -50Repository: 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 3Repository: 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 -100Repository: 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 2Repository: 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.ktRepository: 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 5Repository: 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.
...c/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
Outdated
Show resolved
Hide resolved
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.
There was a problem hiding this 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 declaresQUERY_ALL_PACKAGESin 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 beandroidContext()for context injection.The
ShizukuManagerprovider usescontext = get()which will cause a runtime exception because noContextbinding is registered in the DI graph. All other providers in this module correctly useandroidContext().🐛 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 returnsfalseimmediately. The state only updates in thegrantedbranch (line 535-538), so if the user grants permission through the dialog,isShizukuAvailableremains stale until the screen is reloaded. Consider observingShizukuManager.hasPermissionStateFlow 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 intry/catchwith 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 byinstallViaShizukuis never collected.The
install()method callsinstallViaShizuku(file)which returns aFlow<InstallationProgress>, but this flow is never collected. Since Kotlin flows are cold, the installation will not execute when Shizuku is available andinstall()is called directly.Note: The ViewModel correctly uses
installWithShizukuProgress()directly for Shizuku installations, so this may not be hit in practice. However, theinstall()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: Callcleanup()during app shutdown to prevent listener leak.The
initblock registers three listeners on the Shizuku singleton. SinceShizukuManageris 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 becauseLaunchedEffectcancels when the composable leaves composition, but consider addingisActivecheck 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:
isShizukuAvailablevsisShizukuRunningisShizukuEnabledvsisShizukuInstalledThis 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.
installWithShizukuProgressthrowsUnsupportedOperationExceptionwhile other Shizuku methods returnfalse. This inconsistency could lead to unexpected crashes if a caller doesn't checkisShizukuAvailable()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/-1is 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -5Repository: 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 -100Repository: 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:
- Using
okio.Path(KMP-compatible) - Using an expect/actual pattern with a platform-agnostic abstraction
- 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.
| 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) | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the DetailsState definition to see what fields it has
fd "DetailsState" --type=kt | head -20Repository: 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 15Repository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Find and examine ShizukuSetupDialog definition
fd "ShizukuSetupDialog" --type=kt | head -20Repository: 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 15Repository: rainxchzed/Github-Store
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Find DetailsState in kotlin files
rg "data class DetailsState|class DetailsState" -A 20Repository: rainxchzed/Github-Store
Length of output: 2927
🏁 Script executed:
#!/bin/bash
# Find ShizukuSetupDialog definition
rg "@Composable.*fun ShizukuSetupDialog|fun ShizukuSetupDialog" -A 15Repository: 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 2Repository: rainxchzed/Github-Store
Length of output: 29150
Fix incorrect state field mapping in ShizukuSetupDialog call.
The prop mapping is incorrect:
isShizukuInstalled = state.isShizukuEnabledshould bestate.isShizukuInstalledisShizukuRunning = state.isShizukuAvailableshould bestate.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.
| val isShizukuEnabled = platform.type == PlatformType.ANDROID | ||
| val isShizukuInstalled = installer.isShizukuInstalled() | ||
| val isShizukuRunning = installer.isShizukuAvailable() | ||
| val hasShizukuPermission = installer.isShizukuAvailable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
isShizukuRunningshould reflect whether Shizuku's binder is alive (i.e.,pingBinder()succeeds).hasShizukuPermissionshould 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 permissionAlternatively, 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).
...c/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt
Outdated
Show resolved
Hide resolved
…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.
There was a problem hiding this 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, andautoUpdateFailReasonare new columns; a version bump plusALTER 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 inrefreshShizukuStatusInternal.Lines 644-650 repeat the same issue where
isShizukuRunningandhasShizukuPermissionboth useinstaller.isShizukuAvailable(). This should be fixed alongside the initial load at lines 216-227.
216-227:isShizukuRunningandhasShizukuPermissionstill 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 falseThe UI may incorrectly show "not running" when Shizuku is running but permission is missing. Consider exposing separate methods like
installer.isShizukuRunning()andinstaller.hasShizukuPermission().
🧹 Nitpick comments (4)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt (1)
25-32: Consider consolidatingRequestShizukuPermissionandOnShizukuRequestPermission.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) andOnShizukuRequestPermission(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.
enabledis derived fromisDownloading/isInstalling, whileisActiveDownloadis 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 foldingisActiveDownloadinto 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 + }
| class ShizukuManager( | ||
| private val context: Context | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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' -C3Repository: 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.ktRepository: 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.ktRepository: 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 -20Repository: 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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "isPendingInstall" --type kotlin -A 3 -B 3Repository: 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.
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.
ShizukuInstallerandShizukuManagerto handle the installation logic and binder lifecycle.BroadcastReceiverto handle installation results from the Android Package Installer.AutoUpdateWorkerto periodically check for and install updates in the background for apps with auto-update enabled.AndroidInstallerto a dedicatedinstallerpackage and refactored it to use the new Shizuku services as the primary installation method, with a fallback to the standard package installer.AndroidManifest.xmlwith permissions and components required for Shizuku and background work.Summary by CodeRabbit
New Features
Enhancements
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.