From fbc6fbe2abd8f171bcd327a512d1ca7cc46dcbbf Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Fri, 11 Apr 2025 19:10:49 -0700 Subject: [PATCH] fix(HomeScreen): immediately show installations regardless of remote data fetching This changes state handling to update the up-to-date status after the remote has completed. This ensures that the already-fetched installations are immediately shown in the UI regardless of up-to-date status. --- .../manager/ui/screens/home/HomeModel.kt | 172 +++++++++++------- .../manager/ui/screens/home/HomeScreen.kt | 4 +- .../manager/ui/screens/home/InstallData.kt | 2 +- .../home/components/InstalledItemCard.kt | 3 +- .../ui/screens/patching/PatchingScreen.kt | 3 +- .../screens/patching/PatchingScreenModel.kt | 9 +- 6 files changed, 117 insertions(+), 76 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt index 209d519..b01d3e1 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeModel.kt @@ -24,11 +24,14 @@ import com.aliucord.manager.patcher.InstallMetadata import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen import com.aliucord.manager.ui.util.DiscordVersion +import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.util.launchBlock import com.aliucord.manager.util.showToast import com.github.diamondminer88.zip.ZipReader -import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -42,18 +45,33 @@ class HomeModel( var supportedVersion by mutableStateOf(DiscordVersion.None) private set - var state by mutableStateOf(InstallsState.Fetching) + var installsState by mutableStateOf(InstallsState.Fetching) private set + private val refreshingLock = Mutex() private var remoteDataJson: BuildInfo? = null private var latestAliuhookVersion: SemVer? = null init { - // fetchInstallations() is also called from UI first to ensure fast TTI + refresh() + } - screenModelScope.launch { - fetchRemoteData() - fetchInstallations() // Re-fetch installations to set the up-to-date statuses + fun refresh() = screenModelScope.launchBlock { + if (refreshingLock.isLocked) return@launchBlock + + refreshingLock.withLock { + val packages = fetchAliucordPackages() + + val jobs = listOf( + screenModelScope.launch { fetchInstallations(packages) }, + screenModelScope.launch { + if (remoteDataJson == null || latestAliuhookVersion == null) + fetchRemoteData() + } + ) + + jobs.joinAll() + refreshInstallationsUpToDate(packages) } } @@ -76,60 +94,6 @@ class HomeModel( application.startActivity(launchIntent) } - fun fetchInstallations() = screenModelScope.launchBlock { - state = InstallsState.Fetching - - try { - val packageManager = application.packageManager - - val installedPackages = packageManager - .getInstalledPackages(PackageManager.GET_META_DATA) - .takeIf { it.isNotEmpty() } - ?: throw IllegalStateException("Failed to fetch installed packages (returned none)") - - val aliucordPackages = installedPackages - .asSequence() - .filter { - val isAliucordPkg = it.packageName == "com.aliucord" // Legacy installer builds do not have isAliucord metadata marker - val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true - isAliucordPkg || hasAliucordMeta - } - - val aliucordInstallations = aliucordPackages.mapNotNull { - // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor` - @Suppress("DEPRECATION") - val versionCode = it.versionCode - val versionName = it.versionName ?: return@mapNotNull null - val applicationInfo = it.applicationInfo ?: return@mapNotNull null - - InstallData( - name = packageManager.getApplicationLabel(applicationInfo).toString(), - packageName = it.packageName, - isUpToDate = isInstallationUpToDate(it), - icon = packageManager - .getApplicationIcon(applicationInfo) - .toBitmap() - .asImageBitmap() - .let(::BitmapPainter), - version = DiscordVersion.Existing( - type = DiscordVersion.parseVersionType(versionCode), - name = versionName.split("-")[0].trim(), - code = versionCode, - ), - ) - }.toImmutableList() - - state = if (aliucordInstallations.isNotEmpty()) { - InstallsState.Fetched(data = aliucordInstallations) - } else { - InstallsState.None - } - } catch (t: Throwable) { - Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t) - state = InstallsState.Error - } - } - /** * Creates a [PatchOptionsScreen] that can be navigated to, * with prefilled options from an existing installation. @@ -153,7 +117,68 @@ class HomeModel( return PatchOptionsScreen(prefilledOptions = patchOptions) } - suspend fun fetchRemoteData() { + private fun fetchInstallations(packages: List) { + installsState = InstallsState.Fetching + + try { + val packageManager = application.packageManager + val aliucordInstallations = packages.mapNotNull { pkg -> + // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor` + @Suppress("DEPRECATION") + val versionCode = pkg.versionCode + val versionName = pkg.versionName ?: return@mapNotNull null + val applicationInfo = pkg.applicationInfo ?: return@mapNotNull null + + InstallData( + name = packageManager.getApplicationLabel(applicationInfo).toString(), + packageName = pkg.packageName, + isUpToDate = isInstallationUpToDate(pkg), + icon = packageManager + .getApplicationIcon(applicationInfo) + .toBitmap() + .asImageBitmap() + .let(::BitmapPainter), + version = DiscordVersion.Existing( + type = DiscordVersion.parseVersionType(versionCode), + name = versionName.split("-")[0].trim(), + code = versionCode, + ), + ) + } + + installsState = if (aliucordInstallations.isNotEmpty()) { + InstallsState.Fetched(data = aliucordInstallations.toUnsafeImmutable()) + } else { + InstallsState.None + } + } catch (t: Throwable) { + Log.e(BuildConfig.TAG, "Failed to query Aliucord installations", t) + installsState = InstallsState.Error + } + } + + private fun refreshInstallationsUpToDate(packages: List) { + val installations = (installsState as? InstallsState.Fetched)?.data + ?: return + + try { + val newInstallations = installations.map { data -> + val packageInfo = packages.find { it.packageName == data.packageName } + ?: throw IllegalStateException("Checking up-to-date status for package that has not been fetched") + + data.copy(isUpToDate = isInstallationUpToDate(packageInfo)) + } + + installsState = InstallsState.Fetched(data = newInstallations.toUnsafeImmutable()) + } catch (t: Throwable) { + Log.e(BuildConfig.TAG, "Failed to check installations up-to-date", t) + installsState = InstallsState.Error + } + } + + // FIXME: Properly show state when one of these fetches fail, + // to ensure the people know that the up-to-date check does not work + private suspend fun fetchRemoteData() { github.getBuildData().fold( success = { val versionCode = it.discordVersionCode.toIntOrNull() @@ -184,10 +209,29 @@ class HomeModel( ) } - private fun isInstallationUpToDate(pkg: PackageInfo): Boolean { + /** + * Obtains all installed packages on the device that are an Aliucord installation. + */ + private fun fetchAliucordPackages(): List { + return application.packageManager + .getInstalledPackages(PackageManager.GET_META_DATA) + .filter { + // Packages installed via the legacy Installer do not have the metadata marker + val isAliucordPkg = it.packageName == "com.aliucord" + val hasAliucordMeta = it.applicationInfo?.metaData?.containsKey("isAliucord") == true + isAliucordPkg || hasAliucordMeta + } + } + + /** + * Attempts to determine whether the Aliucord installation is up-to-date. + * If `null` is returned, then the build data was not fetched, and as such + * the status cannot be determined. + */ + private fun isInstallationUpToDate(pkg: PackageInfo): Boolean? { // Assume up-to-date when remote data hasn't been fetched yet - val remoteBuildData = remoteDataJson ?: return true - val latestAliuhookVersion = latestAliuhookVersion ?: return true + val remoteBuildData = remoteDataJson ?: return null + val latestAliuhookVersion = latestAliuhookVersion ?: return null // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor` @Suppress("DEPRECATION") diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt index 59291e2..053902a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/HomeScreen.kt @@ -52,7 +52,7 @@ class HomeScreen : Screen, Parcelable { // Refresh installations list when the screen changes or activity resumes LifecycleResumeEffect(Unit) { - model.fetchInstallations() + model.refresh() onPauseOrDispose {} } @@ -60,7 +60,7 @@ class HomeScreen : Screen, Parcelable { Scaffold( topBar = { HomeAppBar() }, ) { padding -> - when (val state = model.state) { + when (val state = model.installsState) { is InstallsState.Fetched -> HomeScreenLoadedContent( state = state, supportedVersion = model.supportedVersion, diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt index 633e55b..bd8b593 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/InstallData.kt @@ -10,5 +10,5 @@ data class InstallData( val packageName: String, val version: DiscordVersion, val icon: BitmapPainter, - val isUpToDate: Boolean, + val isUpToDate: Boolean?, ) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt index 523f179..8b386ec 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstalledItemCard.kt @@ -99,7 +99,8 @@ fun InstalledItemCard( onClick = onOpenInfo, ) - if (data.isUpToDate) { + // If the up-to-date status cannot be determined, assume it is up-to-date + if (data.isUpToDate ?: true) { SegmentedButton( icon = painterResource(R.drawable.ic_launch), text = stringResource(R.string.action_launch), diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt index 2c73b46..f53f43e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt @@ -32,6 +32,7 @@ import com.aliucord.manager.R import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.ui.components.* import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog +import com.aliucord.manager.ui.screens.log.LogScreen import com.aliucord.manager.ui.screens.patching.components.* import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.util.paddings.* @@ -251,7 +252,7 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable { MainActionButton( text = stringResource(R.string.action_open_error_log), icon = painterResource(R.drawable.ic_launch), - onClick = { model.openLog(navigator) }, + onClick = { navigator.push(LogScreen(installId = model.getCurrentInstallId()!!)) }, ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt index 966fe9b..dd3d84c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt @@ -7,7 +7,6 @@ import androidx.annotation.StringRes import androidx.compose.runtime.* import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import cafe.adriel.voyager.navigator.Navigator import com.aliucord.manager.BuildConfig import com.aliucord.manager.R import com.aliucord.manager.manager.* @@ -16,7 +15,6 @@ import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.base.* import com.aliucord.manager.patcher.steps.install.InstallStep -import com.aliucord.manager.ui.screens.log.LogScreen import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.util.launchBlock @@ -64,6 +62,8 @@ class PatchingScreenModel( } } + fun getCurrentInstallId(): String? = installId + fun launchApp() { if (state.value !is PatchingScreenState.Success) return @@ -121,11 +121,6 @@ class PatchingScreenModel( } } - fun openLog(navigator: Navigator) { - val id = installId ?: return - navigator.push(LogScreen(installId = id)) - } - private suspend fun startPatchRunner() { val runner = KotlinPatchRunner(options) .also { stepRunner = it }