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.
This commit is contained in:
rushiiMachine 2025-04-11 19:10:49 -07:00
parent a9d894eae2
commit fbc6fbe2ab
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
6 changed files with 117 additions and 76 deletions

View file

@ -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>(DiscordVersion.None)
private set
var state by mutableStateOf<InstallsState>(InstallsState.Fetching)
var installsState by mutableStateOf<InstallsState>(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<PackageInfo>) {
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<PackageInfo>) {
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<PackageInfo> {
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")

View file

@ -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,

View file

@ -10,5 +10,5 @@ data class InstallData(
val packageName: String,
val version: DiscordVersion,
val icon: BitmapPainter,
val isUpToDate: Boolean,
val isUpToDate: Boolean?,
)

View file

@ -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),

View file

@ -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()!!)) },
)
}
}

View file

@ -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 }