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:
parent
a9d894eae2
commit
fbc6fbe2ab
6 changed files with 117 additions and 76 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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()!!)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue