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.PatchOptions
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
import com.aliucord.manager.ui.util.DiscordVersion 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.launchBlock
import com.aliucord.manager.util.showToast import com.aliucord.manager.util.showToast
import com.github.diamondminer88.zip.ZipReader import com.github.diamondminer88.zip.ZipReader
import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
@ -42,18 +45,33 @@ class HomeModel(
var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None) var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None)
private set private set
var state by mutableStateOf<InstallsState>(InstallsState.Fetching) var installsState by mutableStateOf<InstallsState>(InstallsState.Fetching)
private set private set
private val refreshingLock = Mutex()
private var remoteDataJson: BuildInfo? = null private var remoteDataJson: BuildInfo? = null
private var latestAliuhookVersion: SemVer? = null private var latestAliuhookVersion: SemVer? = null
init { init {
// fetchInstallations() is also called from UI first to ensure fast TTI refresh()
}
screenModelScope.launch { fun refresh() = screenModelScope.launchBlock {
fetchRemoteData() if (refreshingLock.isLocked) return@launchBlock
fetchInstallations() // Re-fetch installations to set the up-to-date statuses
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) 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, * Creates a [PatchOptionsScreen] that can be navigated to,
* with prefilled options from an existing installation. * with prefilled options from an existing installation.
@ -153,7 +117,68 @@ class HomeModel(
return PatchOptionsScreen(prefilledOptions = patchOptions) 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( github.getBuildData().fold(
success = { success = {
val versionCode = it.discordVersionCode.toIntOrNull() 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 // Assume up-to-date when remote data hasn't been fetched yet
val remoteBuildData = remoteDataJson ?: return true val remoteBuildData = remoteDataJson ?: return null
val latestAliuhookVersion = latestAliuhookVersion ?: return true val latestAliuhookVersion = latestAliuhookVersion ?: return null
// `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor` // `longVersionCode` is unnecessary since Discord doesn't use `versionCodeMajor`
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View file

@ -52,7 +52,7 @@ class HomeScreen : Screen, Parcelable {
// Refresh installations list when the screen changes or activity resumes // Refresh installations list when the screen changes or activity resumes
LifecycleResumeEffect(Unit) { LifecycleResumeEffect(Unit) {
model.fetchInstallations() model.refresh()
onPauseOrDispose {} onPauseOrDispose {}
} }
@ -60,7 +60,7 @@ class HomeScreen : Screen, Parcelable {
Scaffold( Scaffold(
topBar = { HomeAppBar() }, topBar = { HomeAppBar() },
) { padding -> ) { padding ->
when (val state = model.state) { when (val state = model.installsState) {
is InstallsState.Fetched -> HomeScreenLoadedContent( is InstallsState.Fetched -> HomeScreenLoadedContent(
state = state, state = state,
supportedVersion = model.supportedVersion, supportedVersion = model.supportedVersion,

View file

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

View file

@ -99,7 +99,8 @@ fun InstalledItemCard(
onClick = onOpenInfo, 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( SegmentedButton(
icon = painterResource(R.drawable.ic_launch), icon = painterResource(R.drawable.ic_launch),
text = stringResource(R.string.action_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.patcher.steps.StepGroup
import com.aliucord.manager.ui.components.* import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog 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.patching.components.*
import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.screens.patchopts.PatchOptions
import com.aliucord.manager.ui.util.paddings.* import com.aliucord.manager.ui.util.paddings.*
@ -251,7 +252,7 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
MainActionButton( MainActionButton(
text = stringResource(R.string.action_open_error_log), text = stringResource(R.string.action_open_error_log),
icon = painterResource(R.drawable.ic_launch), 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 androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.manager.* 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.StepGroup
import com.aliucord.manager.patcher.steps.base.* import com.aliucord.manager.patcher.steps.base.*
import com.aliucord.manager.patcher.steps.install.InstallStep 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.screens.patchopts.PatchOptions
import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.launchBlock import com.aliucord.manager.util.launchBlock
@ -64,6 +62,8 @@ class PatchingScreenModel(
} }
} }
fun getCurrentInstallId(): String? = installId
fun launchApp() { fun launchApp() {
if (state.value !is PatchingScreenState.Success) if (state.value !is PatchingScreenState.Success)
return return
@ -121,11 +121,6 @@ class PatchingScreenModel(
} }
} }
fun openLog(navigator: Navigator) {
val id = installId ?: return
navigator.push(LogScreen(installId = id))
}
private suspend fun startPatchRunner() { private suspend fun startPatchRunner() {
val runner = KotlinPatchRunner(options) val runner = KotlinPatchRunner(options)
.also { stepRunner = it } .also { stepRunner = it }