Skip to content

Commit 72976d3

Browse files
committed
fix: Replace isLoaded flag with BundleState sealed class and simplify homeAppState
1 parent e93709b commit 72976d3

2 files changed

Lines changed: 73 additions & 73 deletions

File tree

app/src/main/java/app/morphe/manager/domain/repository/PatchBundleRepository.kt

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import app.morphe.manager.data.room.bundles.PatchBundleProperties
1717
import app.morphe.manager.data.room.bundles.Source
1818
import app.morphe.manager.domain.bundles.*
1919
import app.morphe.manager.domain.manager.PreferencesManager
20-
import app.morphe.manager.patcher.patch.PatchBundle
20+
import app.morphe.manager.network.utils.APIError
2121
import app.morphe.manager.patcher.patch.BundleAppMetadata
22+
import app.morphe.manager.patcher.patch.PatchBundle
2223
import app.morphe.manager.patcher.patch.PatchBundleInfo
24+
import app.morphe.manager.ui.viewmodel.BundleSnapshot
2325
import app.morphe.manager.util.*
26+
import io.ktor.client.plugins.ResponseException
2427
import io.ktor.http.Url
2528
import kotlinx.collections.immutable.PersistentMap
2629
import kotlinx.collections.immutable.mutate
@@ -40,10 +43,6 @@ import java.nio.ByteOrder
4043
import java.nio.charset.StandardCharsets
4144
import java.security.MessageDigest
4245
import java.util.Locale
43-
import app.morphe.manager.util.syncFcmTopics
44-
import app.morphe.manager.network.utils.APIError
45-
import app.morphe.manager.ui.viewmodel.BundleSnapshot
46-
import io.ktor.client.plugins.ResponseException
4746
import app.morphe.manager.data.room.bundles.Source as SourceInfo
4847

4948
class PatchBundleRepository(
@@ -56,15 +55,18 @@ class PatchBundleRepository(
5655
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
5756

5857
private val scope = CoroutineScope(Dispatchers.Default)
59-
private val store = Store(scope, State())
58+
private val store = Store<BundleState>(scope, BundleState.Loading)
6059

61-
val sources = store.state.map { it.sources.values.toList() }
60+
val bundleState: StateFlow<BundleState> = store.state
61+
.stateIn(scope, SharingStarted.Eagerly, BundleState.Loading)
62+
63+
val sources = store.state.map { (it as? BundleState.Ready)?.sources?.values?.toList() ?: emptyList() }
6264
val bundles = store.state.map {
63-
it.sources.mapNotNull { (uid, src) ->
65+
(it as? BundleState.Ready)?.sources?.mapNotNull { (uid, src) ->
6466
uid to (src.patchBundle ?: return@mapNotNull null)
65-
}.toMap()
67+
}?.toMap() ?: emptyMap()
6668
}
67-
val allBundlesInfoFlow = store.state.map { it.info }
69+
val allBundlesInfoFlow = store.state.map { (it as? BundleState.Ready)?.info ?: persistentMapOf() }
6870
val enabledBundlesInfoFlow = allBundlesInfoFlow.map { info ->
6971
info.filter { (_, bundleInfo) -> bundleInfo.enabled }
7072
}
@@ -80,9 +82,6 @@ class PatchBundleRepository(
8082
.map { BundleAppMetadata.buildFrom(it) }
8183
.stateIn(scope, SharingStarted.Eagerly, emptyMap())
8284

83-
/** True once [doReload] has completed at least once (even if all bundles are disabled). */
84-
val isBundlePipelineLoaded: Flow<Boolean> = store.state.map { it.isLoaded }
85-
8685
fun scopedBundleInfoFlow(packageName: String, version: String?) = enabledBundlesInfoFlow.map {
8786
it.map { (_, bundleInfo) ->
8887
bundleInfo.forPackage(
@@ -324,18 +323,18 @@ class PatchBundleRepository(
324323

325324
private suspend inline fun dispatchAction(
326325
name: String,
327-
crossinline block: suspend ActionContext.(current: State) -> State
326+
crossinline block: suspend ActionContext.(current: BundleState) -> BundleState
328327
) {
329-
store.dispatch(object : Action<State> {
330-
override suspend fun ActionContext.execute(current: State) = block(current)
328+
store.dispatch(object : Action<BundleState> {
329+
override suspend fun ActionContext.execute(current: BundleState) = block(current)
331330
override fun toString() = name
332331
})
333332
}
334333

335334
/**
336335
* Performs a reload. Do not call this outside of a store action.
337336
*/
338-
private suspend fun doReload(): State {
337+
private suspend fun doReload(): BundleState.Ready {
339338
val entities = loadEntitiesEnforcingOfficialOrder()
340339

341340
val sources = entities.associate { it.uid to it.load() }.toMutableMap()
@@ -344,22 +343,23 @@ class PatchBundleRepository(
344343
if (hasOutOfDateNames) dispatchAction(
345344
"Sync names"
346345
) { state ->
347-
val nameChanges = state.sources.mapNotNull { (_, src) ->
346+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
347+
val nameChanges = ready.sources.mapNotNull { (_, src) ->
348348
if (!src.isNameOutOfDate) return@mapNotNull null
349349
val newName = src.patchBundle?.manifestAttributes?.name?.takeIf { it != src.name }
350350
?: return@mapNotNull null
351351

352352
src.uid to newName
353353
}
354-
val sources = state.sources.toMutableMap()
355-
val info = state.info.toMutableMap()
354+
val sources = ready.sources.toMutableMap()
355+
val info = ready.info.toMutableMap()
356356
nameChanges.forEach { (uid, name) ->
357357
updateDb(uid) { it.copy(name = name) }
358358
sources[uid] = sources[uid]!!.copy(name = name)
359359
info[uid] = info[uid]?.copy(name = name) ?: return@forEach
360360
}
361361

362-
State(sources.toPersistentMap(), info.toPersistentMap())
362+
ready.copy(sources = sources.toPersistentMap(), info = info.toPersistentMap())
363363
}
364364
val info = loadMetadata(sources).toMutableMap()
365365

@@ -377,7 +377,7 @@ class PatchBundleRepository(
377377
}
378378
}
379379

380-
return State(sources.toPersistentMap(), info.toPersistentMap(), isLoaded = true)
380+
return BundleState.Ready(sources.toPersistentMap(), info.toPersistentMap())
381381
}
382382

383383
suspend fun reload() = dispatchAction("Full reload") {
@@ -424,7 +424,8 @@ class PatchBundleRepository(
424424

425425
if (failures.isNotEmpty()) {
426426
dispatchAction("Mark bundles as failed") { state ->
427-
state.copy(sources = state.sources.mutate {
427+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
428+
ready.copy(sources = ready.sources.mutate {
428429
failures.forEach { (uid, throwable) ->
429430
it[uid] = it[uid]?.copy(error = throwable) ?: return@forEach
430431
}
@@ -601,7 +602,7 @@ class PatchBundleRepository(
601602

602603
suspend fun reset() = dispatchAction("Reset") { state ->
603604
dao.reset()
604-
state.sources.keys.forEach { directoryOf(it).deleteRecursively() }
605+
(state as? BundleState.Ready)?.sources?.keys?.forEach { directoryOf(it).deleteRecursively() }
605606
doReload()
606607
}
607608

@@ -694,8 +695,9 @@ class PatchBundleRepository(
694695

695696
suspend fun remove(vararg bundles: PatchBundleSource) =
696697
dispatchAction("Remove (${bundles.map { it.uid }.joinToString(",")})") { state ->
697-
val sources = state.sources.toMutableMap()
698-
val info = state.info.toMutableMap()
698+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
699+
val sources = ready.sources.toMutableMap()
700+
val info = ready.info.toMutableMap()
699701
bundles.forEach {
700702
dao.remove(it.uid)
701703
directoryOf(it.uid).deleteRecursively()
@@ -706,7 +708,7 @@ class PatchBundleRepository(
706708
val (affectedCount, remaining) = cancelRemoteUpdates(bundles.map { it.uid }.toSet())
707709
updateProgressAfterRemoval(affectedCount, remaining)
708710

709-
State(sources.toPersistentMap(), info.toPersistentMap(), isLoaded = state.isLoaded)
711+
ready.copy(sources = sources.toPersistentMap(), info = info.toPersistentMap())
710712
}
711713

712714
enum class DisplayNameUpdateResult {
@@ -753,9 +755,10 @@ class PatchBundleRepository(
753755

754756
if (result == DisplayNameUpdateResult.SUCCESS || result == DisplayNameUpdateResult.NO_CHANGE) {
755757
dispatchAction("Sync display name ($uid)") { state ->
756-
val src = state.sources[uid] ?: return@dispatchAction state
758+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
759+
val src = ready.sources[uid] ?: return@dispatchAction state
757760
val updated = src.copy(displayName = normalized)
758-
state.copy(sources = state.sources.put(uid, updated))
761+
ready.copy(sources = ready.sources.put(uid, updated))
759762
}
760763
}
761764

@@ -773,13 +776,14 @@ class PatchBundleRepository(
773776
prefs.bundlePrereleasesEnabled.update(current)
774777

775778
dispatchAction("Set prerelease ($uid=$usePrerelease)") { state ->
776-
val src = state.sources[uid] ?: return@dispatchAction state
779+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
780+
val src = ready.sources[uid] ?: return@dispatchAction state
777781
val updated = when (src) {
778782
is APIPatchBundle -> src.copy(usePrerelease = usePrerelease)
779783
is JsonPatchBundle -> src.copy(usePrerelease = usePrerelease)
780784
else -> return@dispatchAction state
781785
}
782-
state.copy(sources = state.sources.put(uid, updated))
786+
ready.copy(sources = ready.sources.put(uid, updated))
783787
}
784788

785789
// If this is the default Morphe Patches bundle, sync FCM patches topic
@@ -794,7 +798,7 @@ class PatchBundleRepository(
794798

795799
// Skip download if the bundle is disabled - it will be downloaded when re-enabled
796800
// via disable() which triggers startRemoteUpdateJob for newly enabled bundles.
797-
val isEnabled = store.state.value.sources[uid]?.enabled == true
801+
val isEnabled = (store.state.value as? BundleState.Ready)?.sources?.get(uid)?.enabled == true
798802
if (!isEnabled) return
799803

800804
// Trigger update so the new channel takes effect immediately.
@@ -1049,7 +1053,9 @@ class PatchBundleRepository(
10491053

10501054

10511055
// Check for duplicate source
1052-
val isDuplicate = state.sources.values.any { src ->
1056+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
1057+
1058+
val isDuplicate = ready.sources.values.any { src ->
10531059
src is RemotePatchBundle && src.endpoint.equals(normalizedUrl, ignoreCase = true)
10541060
}
10551061

@@ -1084,7 +1090,7 @@ class PatchBundleRepository(
10841090
if (bundle.uid == src.uid) onProgress?.invoke(bytesRead, bytesTotal)
10851091
}
10861092
)
1087-
state.copy(sources = state.sources.put(src.uid, src))
1093+
ready.copy(sources = ready.sources.put(src.uid, src))
10881094
}
10891095

10901096
/**
@@ -1315,7 +1321,8 @@ class PatchBundleRepository(
13151321
if (!allowMeteredUpdates && networkInfo.isMetered()) return null
13161322

13171323
return try {
1318-
val remoteBundles = store.state.value.sources.values
1324+
val remoteBundles = (store.state.value as? BundleState.Ready)?.sources?.values
1325+
.orEmpty()
13191326
.filterIsInstance<RemotePatchBundle>()
13201327

13211328
if (remoteBundles.isEmpty()) return null
@@ -1360,12 +1367,12 @@ class PatchBundleRepository(
13601367
private val allowUnsafeNetwork: Boolean = false,
13611368
private val onPerBundleProgress: ((bundle: RemotePatchBundle, bytesRead: Long, bytesTotal: Long?) -> Unit)? = null,
13621369
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
1363-
) : Action<State> {
1370+
) : Action<BundleState> {
13641371
override fun toString() = if (force) "Redownload remote bundles" else "Update check"
13651372

13661373
override suspend fun ActionContext.execute(
1367-
current: State
1368-
): State {
1374+
current: BundleState
1375+
): BundleState {
13691376
startRemoteUpdateJob(
13701377
force = force,
13711378
showToast = showToast,
@@ -1476,7 +1483,8 @@ class PatchBundleRepository(
14761483
return@coroutineScope
14771484
}
14781485

1479-
val targets = store.state.value.sources.values
1486+
val targets = (store.state.value as? BundleState.Ready)?.sources?.values
1487+
.orEmpty()
14801488
.filterIsInstance<RemotePatchBundle>()
14811489
.filter { predicate(it) }
14821490

@@ -1666,9 +1674,10 @@ class PatchBundleRepository(
16661674

16671675
private inner class ManualUpdateCheck(
16681676
private val targetUids: Set<Int>? = null
1669-
) : Action<State> {
1670-
override suspend fun ActionContext.execute(current: State) = coroutineScope {
1671-
val manualBundles = current.sources.values
1677+
) : Action<BundleState> {
1678+
override suspend fun ActionContext.execute(current: BundleState) = coroutineScope {
1679+
val ready = current as? BundleState.Ready ?: return@coroutineScope current
1680+
val manualBundles = ready.sources.values
16721681
.filterIsInstance<RemotePatchBundle>()
16731682
.filter {
16741683
targetUids?.contains(it.uid) ?: !it.autoUpdate
@@ -1680,7 +1689,7 @@ class PatchBundleRepository(
16801689
} else {
16811690
manualUpdateInfoFlow.update { map ->
16821691
map.filterKeys { uid ->
1683-
val bundle = current.sources[uid] as? RemotePatchBundle
1692+
val bundle = ready.sources[uid] as? RemotePatchBundle
16841693
bundle != null && !bundle.autoUpdate
16851694
}
16861695
}
@@ -1729,12 +1738,16 @@ class PatchBundleRepository(
17291738
}
17301739
}
17311740

1732-
data class State(
1733-
val sources: PersistentMap<Int, PatchBundleSource> = persistentMapOf(),
1734-
val info: PersistentMap<Int, PatchBundleInfo.Global> = persistentMapOf(),
1735-
/** True once the initial DB load has completed (even if all bundles are disabled). */
1736-
val isLoaded: Boolean = false
1737-
)
1741+
sealed class BundleState {
1742+
/** DB not yet read — UI shows shimmer */
1743+
data object Loading : BundleState()
1744+
1745+
/** Pipeline ready (even if sources list is empty) */
1746+
data class Ready(
1747+
val sources: PersistentMap<Int, PatchBundleSource> = persistentMapOf(),
1748+
val info: PersistentMap<Int, PatchBundleInfo.Global> = persistentMapOf(),
1749+
) : BundleState()
1750+
}
17381751

17391752
enum class BundleUpdateResult {
17401753
None, // Update in progress
@@ -1810,7 +1823,8 @@ class PatchBundleRepository(
18101823
suspend fun importCustomBundles(snapshots: List<BundleSnapshot>) {
18111824
if (snapshots.isEmpty()) return
18121825
dispatchAction("Import custom bundles") { state ->
1813-
val existingEndpoints = state.sources.values
1826+
val ready = state as? BundleState.Ready ?: return@dispatchAction state
1827+
val existingEndpoints = ready.sources.values
18141828
.filterIsInstance<RemotePatchBundle>()
18151829
.map { it.endpoint.lowercase(Locale.US) }
18161830
.toSet()

app/src/main/java/app/morphe/manager/ui/viewmodel/HomeViewModel.kt

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ import android.provider.OpenableColumns
1818
import android.util.Log
1919
import android.widget.Toast
2020
import androidx.annotation.RequiresApi
21-
import androidx.compose.runtime.getValue
22-
import androidx.compose.runtime.mutableFloatStateOf
23-
import androidx.compose.runtime.mutableIntStateOf
24-
import androidx.compose.runtime.mutableStateOf
25-
import androidx.compose.runtime.setValue
21+
import androidx.compose.runtime.*
2622
import androidx.lifecycle.ViewModel
2723
import androidx.lifecycle.viewModelScope
2824
import app.morphe.manager.R
@@ -65,7 +61,6 @@ import java.io.File
6561
import java.io.FileNotFoundException
6662
import java.net.URLEncoder.encode
6763
import java.security.MessageDigest
68-
import kotlin.collections.emptyList
6964
import kotlin.time.Clock
7065

7166
/** Bundle update status for snackbar display. */
@@ -159,10 +154,6 @@ class HomeViewModel(
159154
private val contentResolver: ContentResolver = app.contentResolver
160155

161156
/** Becomes true once the bundle repository has finished its initial DB load. */
162-
private val isBundlePipelineLoaded: StateFlow<Boolean> =
163-
patchBundleRepository.isBundlePipelineLoaded
164-
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
165-
166157
/** Android 11 kills the app process after granting the "install apps" permission. */
167158
val android11BugActive get() = Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !pm.canInstallPackages()
168159

@@ -1039,15 +1030,6 @@ class HomeViewModel(
10391030
val bundleAppMetadataFlow: StateFlow<Map<String, BundleAppMetadata>> =
10401031
patchBundleRepository.appMetadata
10411032

1042-
/**
1043-
* Set of all unique package names that have patches across all enabled bundles.
1044-
* Derived from [bundleAppMetadataFlow] keys - no need to re-iterate all patches.
1045-
*/
1046-
val patchablePackagesFlow: StateFlow<Set<String>> =
1047-
bundleAppMetadataFlow
1048-
.map { it.keys }
1049-
.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())
1050-
10511033
/**
10521034
* Combined flow that produces the sorted list of home app items.
10531035
*
@@ -1057,13 +1039,17 @@ class HomeViewModel(
10571039
* Hidden apps are excluded.
10581040
*/
10591041
val homeAppState: StateFlow<HomeAppState?> = combine(
1060-
patchablePackagesFlow,
1042+
patchBundleRepository.bundleState,
10611043
homeAppButtonPrefs.hiddenPackages,
10621044
installedAppRepository.getAll(),
10631045
_appUpdatesAvailable,
1064-
bundleAppMetadataFlow
1065-
) { packages, hiddenPackages, installedApps, updatesMap, metadata ->
1066-
if (!isBundlePipelineLoaded.value) return@combine null
1046+
) { bundleState, hiddenPackages, installedApps, updatesMap ->
1047+
val ready = bundleState as? PatchBundleRepository.BundleState.Ready
1048+
?: return@combine null
1049+
1050+
val enabledInfo = ready.info.filter { (_, info) -> info.enabled }
1051+
val metadata = BundleAppMetadata.buildFrom(enabledInfo)
1052+
val packages = metadata.keys
10671053

10681054
val installedMap = installedApps.associateBy { it.originalPackageName }
10691055

0 commit comments

Comments
 (0)