Skip to content

Commit 6d309d5

Browse files
committed
feat: track installation outcome to improve pending install state
- Introduce `InstallOutcome` enum to distinguish between synchronous (`COMPLETED`) and asynchronous (`DELEGATED_TO_SYSTEM`) installation processes. - Update `Installer.install` signature to return `InstallOutcome`. - Implement `InstallOutcome.COMPLETED` in `ShizukuInstallerWrapper` for successful silent installs. - Implement `InstallOutcome.DELEGATED_TO_SYSTEM` for standard Android and Desktop installers where the OS handles the UI. - Update `DetailsViewModel` to use the installation outcome to more accurately set the `isPendingInstall` flag. - Refactor internal install logic in `DetailsViewModel` to propagate the outcome to repository sync operations.
1 parent 1356b8d commit 6d309d5

5 files changed

Lines changed: 49 additions & 19 deletions

File tree

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import co.touchlab.kermit.Logger
1212
import zed.rainxch.core.domain.model.AssetArchitectureMatcher
1313
import zed.rainxch.core.domain.model.GithubAsset
1414
import zed.rainxch.core.domain.model.SystemArchitecture
15+
import zed.rainxch.core.domain.system.InstallOutcome
1516
import zed.rainxch.core.domain.system.Installer
1617
import zed.rainxch.core.domain.system.InstallerInfoExtractor
1718
import java.io.File
@@ -134,7 +135,7 @@ class AndroidInstaller(
134135
override suspend fun install(
135136
filePath: String,
136137
extOrMime: String,
137-
) {
138+
): InstallOutcome {
138139
val file = File(filePath)
139140
if (!file.exists()) {
140141
throw IllegalStateException("APK file not found: $filePath")
@@ -158,6 +159,8 @@ class AndroidInstaller(
158159
} else {
159160
throw IllegalStateException("No installer available on this device")
160161
}
162+
163+
return InstallOutcome.DELEGATED_TO_SYSTEM
161164
}
162165

163166
override fun uninstall(packageName: String) {

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerWrapper.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import zed.rainxch.core.domain.model.GithubAsset
1111
import zed.rainxch.core.domain.model.InstallerType
1212
import zed.rainxch.core.domain.model.SystemArchitecture
1313
import zed.rainxch.core.domain.repository.TweaksRepository
14+
import zed.rainxch.core.domain.system.InstallOutcome
1415
import zed.rainxch.core.domain.system.Installer
1516
import zed.rainxch.core.domain.system.InstallerInfoExtractor
1617

@@ -97,7 +98,7 @@ class ShizukuInstallerWrapper(
9798
override suspend fun install(
9899
filePath: String,
99100
extOrMime: String,
100-
) {
101+
): InstallOutcome {
101102
Logger.d(TAG) { "install() called — filePath=$filePath, extOrMime=$extOrMime" }
102103
Logger.d(TAG) { "cachedInstallerType=$cachedInstallerType, shizukuStatus=${shizukuServiceManager.status.value}" }
103104

@@ -122,7 +123,7 @@ class ShizukuInstallerWrapper(
122123
Logger.d(TAG) { "Shizuku installPackage() returned: $result" }
123124
if (result == 0) {
124125
Logger.d(TAG) { "Shizuku install SUCCEEDED for: $filePath" }
125-
return
126+
return InstallOutcome.COMPLETED
126127
}
127128
Logger.w(TAG) { "Shizuku install FAILED with code: $result, falling back to standard installer" }
128129
} else {
@@ -137,7 +138,7 @@ class ShizukuInstallerWrapper(
137138

138139
Logger.d(TAG) { "Using standard AndroidInstaller for: $filePath" }
139140
androidInstaller.ensurePermissionsOrThrow(extOrMime)
140-
androidInstaller.install(filePath, extOrMime)
141+
return androidInstaller.install(filePath, extOrMime)
141142
}
142143

143144
override fun uninstall(packageName: String) {

core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import zed.rainxch.core.domain.model.AssetArchitectureMatcher
99
import zed.rainxch.core.domain.model.GithubAsset
1010
import zed.rainxch.core.domain.model.Platform
1111
import zed.rainxch.core.domain.model.SystemArchitecture
12+
import zed.rainxch.core.domain.system.InstallOutcome
1213
import zed.rainxch.core.domain.system.Installer
1314
import zed.rainxch.core.domain.system.InstallerInfoExtractor
1415
import java.awt.Desktop
@@ -353,21 +354,24 @@ class DesktopInstaller(
353354
override suspend fun install(
354355
filePath: String,
355356
extOrMime: String,
356-
) = withContext(Dispatchers.IO) {
357-
val file = File(filePath)
358-
if (!file.exists()) {
359-
throw IllegalStateException("File not found: $filePath")
360-
}
357+
): InstallOutcome =
358+
withContext(Dispatchers.IO) {
359+
val file = File(filePath)
360+
if (!file.exists()) {
361+
throw IllegalStateException("File not found: $filePath")
362+
}
361363

362-
val ext = extOrMime.lowercase().removePrefix(".")
364+
val ext = extOrMime.lowercase().removePrefix(".")
363365

364-
when (platform) {
365-
Platform.WINDOWS -> installWindows(file, ext)
366-
Platform.MACOS -> installMacOS(file, ext)
367-
Platform.LINUX -> installLinux(file, ext)
368-
else -> throw UnsupportedOperationException("Installation not supported on $platform")
366+
when (platform) {
367+
Platform.WINDOWS -> installWindows(file, ext)
368+
Platform.MACOS -> installMacOS(file, ext)
369+
Platform.LINUX -> installLinux(file, ext)
370+
else -> throw UnsupportedOperationException("Installation not supported on $platform")
371+
}
372+
373+
InstallOutcome.DELEGATED_TO_SYSTEM
369374
}
370-
}
371375

372376
private fun installWindows(
373377
file: File,

core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ package zed.rainxch.core.domain.system
33
import zed.rainxch.core.domain.model.GithubAsset
44
import zed.rainxch.core.domain.model.SystemArchitecture
55

6+
/**
7+
* Result of an [Installer.install] call.
8+
*/
9+
enum class InstallOutcome {
10+
/**
11+
* Installation completed synchronously (e.g. Shizuku silent install).
12+
* The package is already installed on the system — no need to wait
13+
* for a broadcast to confirm.
14+
*/
15+
COMPLETED,
16+
17+
/**
18+
* Installation was handed off to the system UI or an external process.
19+
* The caller should treat the install as pending until a
20+
* PACKAGE_ADDED / PACKAGE_REPLACED broadcast confirms it.
21+
*/
22+
DELEGATED_TO_SYSTEM,
23+
}
24+
625
interface Installer {
726
suspend fun isSupported(extOrMime: String): Boolean
827

@@ -11,7 +30,7 @@ interface Installer {
1130
suspend fun install(
1231
filePath: String,
1332
extOrMime: String,
14-
)
33+
): InstallOutcome
1534

1635
fun uninstall(packageName: String)
1736

feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import zed.rainxch.core.domain.repository.InstalledAppsRepository
3636
import zed.rainxch.core.domain.repository.SeenReposRepository
3737
import zed.rainxch.core.domain.repository.StarredRepository
3838
import zed.rainxch.core.domain.repository.TweaksRepository
39+
import zed.rainxch.core.domain.system.InstallOutcome
3940
import zed.rainxch.core.domain.system.Installer
4041
import zed.rainxch.core.domain.system.PackageMonitor
4142
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase
@@ -1192,7 +1193,7 @@ class DetailsViewModel(
11921193
}
11931194
}
11941195

1195-
installer.install(filePath, ext)
1196+
val installOutcome = installer.install(filePath, ext)
11961197

11971198
// Launch attestation check asynchronously (non-blocking)
11981199
launchAttestationCheck(filePath)
@@ -1205,6 +1206,7 @@ class DetailsViewModel(
12051206
releaseTag = releaseTag,
12061207
isUpdate = isUpdate,
12071208
filePath = filePath,
1209+
installOutcome = installOutcome,
12081210
)
12091211
} else {
12101212
viewModelScope.launch {
@@ -1394,6 +1396,7 @@ class DetailsViewModel(
13941396
releaseTag: String,
13951397
isUpdate: Boolean,
13961398
filePath: String,
1399+
installOutcome: InstallOutcome = InstallOutcome.DELEGATED_TO_SYSTEM,
13971400
) {
13981401
try {
13991402
val repo = _state.value.repository ?: return
@@ -1455,7 +1458,7 @@ class DetailsViewModel(
14551458
releaseNotes = "",
14561459
systemArchitecture = installer.detectSystemArchitecture().name,
14571460
fileExtension = assetName.substringAfterLast('.', ""),
1458-
isPendingInstall = true,
1461+
isPendingInstall = installOutcome != InstallOutcome.COMPLETED,
14591462
installedVersionName = apkInfo.versionName,
14601463
installedVersionCode = apkInfo.versionCode,
14611464
latestVersionName = apkInfo.versionName,

0 commit comments

Comments
 (0)