Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/src/main/java/org/mozilla/tryfox/ui/screens/HomeAppCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ internal fun HomeAppCard(
downloadState = selectedApk?.downloadState ?: DownloadState.NotDownloaded,
onDownloadClick = { selectedApk?.let(onDownloadClick) },
onInstallClick = { selectedApk?.let(onInstallClick) },
installState = selectedApk?.let { installStates[it.uniqueKey] } ?: InstallState.Idle,
installState = selectedApk?.let {
effectiveInstallState(app, it, installStates)
} ?: InstallState.Idle,
installDisabled = selectedApk == null,
onOpenClick = onOpenInstalledApp,
debugLabel = "home-card:${selectedApk?.uniqueKey ?: app.name}",
Expand Down Expand Up @@ -270,6 +272,14 @@ internal fun HomeAppCard(
}
}

internal fun effectiveInstallState(
app: AppUiModel,
apk: ApkUiModel,
installStates: Map<String, InstallState>,
): InstallState = installStates[apk.uniqueKey]
?.takeUnless { it is InstallState.Installed && app.installedVersion == null }
?: InstallState.Idle

private const val SHORT_REVISION_LENGTH = 12

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/org/mozilla/tryfox/ui/screens/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
Expand All @@ -41,6 +42,9 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import org.mozilla.tryfox.R

Expand Down Expand Up @@ -69,11 +73,22 @@ fun HomeScreen(
val isRefreshing by homeViewModel.isRefreshing.collectAsState()
val installStates by homeViewModel.installStates.collectAsState()
val pullRefreshState = rememberPullRefreshState(isRefreshing, { homeViewModel.refreshData() })
val lifecycleOwner = LocalLifecycleOwner.current

LaunchedEffect(Unit) {
homeViewModel.initialLoad()
}

DisposableEffect(lifecycleOwner, homeViewModel) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
homeViewModel.refreshInstalledAppStates()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
Expand Down
44 changes: 37 additions & 7 deletions app/src/main/java/org/mozilla/tryfox/ui/screens/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,32 @@ class HomeViewModel(
}

fun refreshData() {
if (!initialLoadStarted) {
initialLoad()
return
}
launchRefresh(hydrateCache = false)
}

fun refreshInstalledAppStates() {
viewModelScope.launch(ioDispatcher) {
val appInfoMap = appInfoMap()
synchronized(appsLock) {
currentAppsByName = currentAppsByName.mapValues { (appName, app) ->
app.withInstalledState(appInfoMap[appName])
}
}
_homeScreenState.update { currentState ->
if (currentState !is HomeScreenState.Loaded) return@update currentState
currentState.copy(
apps = currentState.apps.mapValues { (appName, app) ->
app.withInstalledState(appInfoMap[appName])
},
)
}
}
}

fun selectHomeAppFlavor(family: HomeAppFamily, appName: String) {
if (appName !in family.appNames) return
selectedHomeAppNames = selectedHomeAppNames + (family to appName)
Expand Down Expand Up @@ -289,17 +312,24 @@ class HomeViewModel(
AppUiModel(
name = appName,
packageName = appState.packageName,
installedVersion = appState.version,
installedVersionCode = appState.versionCode,
installedDate = appState.formattedInstallDate,
installingPackageName = appState.installingPackageName,
splitNames = appState.splitNames,
installedTryBuild = appName.takeIf { it == FENIX_DEBUG }
?.let { matchingInstalledTryBuild(appState) },
installedVersion = null,
installedDate = null,
apks = ApksResult.Loading,
)
.withInstalledState(appState)
}

private fun AppUiModel.withInstalledState(appState: AppState?): AppUiModel = copy(
packageName = appState?.packageName ?: packageName,
installedVersion = appState?.version,
installedVersionCode = appState?.versionCode,
installedDate = appState?.formattedInstallDate,
installingPackageName = appState?.installingPackageName,
splitNames = appState?.splitNames ?: emptyList(),
installedTryBuild = appState?.takeIf { name == FENIX_DEBUG }
?.let { matchingInstalledTryBuild(it) },
)

private fun publishCurrentApps() {
val apps = synchronized(appsLock) { currentAppsByName }
val currentCacheState = cacheManager.cacheState.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ import org.mozilla.tryfox.util.REFERENCE_BROWSER_PACKAGE
import org.mozilla.tryfox.util.TRYFOX_PACKAGE

class FakeMozillaPackageManager(
private val apps: Map<String, AppState> = emptyMap(),
apps: Map<String, AppState> = emptyMap(),
) : MozillaPackageManager {

private val apps = apps.toMutableMap()

fun setAppState(appState: AppState) {
apps[appState.packageName] = appState
}

override val fenix: AppState
get() = apps[FENIX_NIGHTLY_PACKAGE] ?: AppState("Fenix", FENIX_NIGHTLY_PACKAGE, null, null)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package org.mozilla.tryfox.ui.screens

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.mozilla.tryfox.data.DownloadState
import org.mozilla.tryfox.install.InstallState
import org.mozilla.tryfox.model.HomeScreenLayout
import org.mozilla.tryfox.ui.models.AbiUiModel
import org.mozilla.tryfox.ui.models.ApkUiModel
import org.mozilla.tryfox.ui.models.ApksResult
import org.mozilla.tryfox.ui.models.AppUiModel
import org.mozilla.tryfox.util.FENIX
Expand All @@ -14,6 +18,7 @@ import org.mozilla.tryfox.util.FOCUS_BETA
import org.mozilla.tryfox.util.FOCUS_DEBUG
import org.mozilla.tryfox.util.FOCUS_RELEASE
import org.mozilla.tryfox.util.REFERENCE_BROWSER
import java.io.File

class HomeAppCardModelTest {
@Test
Expand Down Expand Up @@ -79,11 +84,43 @@ class HomeAppCardModelTest {
assertEquals(true, cards.all { !it.showFlavorSelector })
}

@Test
fun `ignores stale installed state when package is no longer installed`() {
val app = app(FENIX)
val apk = apk()
val installState = mapOf(apk.uniqueKey to InstallState.Installed(FENIX))

assertEquals(InstallState.Idle, effectiveInstallState(app, apk, installState))
}

@Test
fun `keeps installed state when package is still installed`() {
val app = app(FENIX, installedVersion = "1.0")
val apk = apk()
val installState = mapOf(apk.uniqueKey to InstallState.Installed(FENIX))

assertEquals(InstallState.Installed(FENIX), effectiveInstallState(app, apk, installState))
}

private fun app(name: String, installedVersion: String? = null) = AppUiModel(
name = name,
packageName = name,
installedVersion = installedVersion,
installedDate = null,
apks = ApksResult.Loading,
)

private fun apk() = ApkUiModel(
originalString = "fenix.apk",
date = "",
buildDate = null,
appName = FENIX,
version = "1.0",
abi = AbiUiModel("arm64-v8a", true),
url = "https://example.com/fenix.apk",
fileName = "fenix.apk",
downloadState = DownloadState.Downloaded(File("fenix.apk")),
uniqueKey = "fenix/fenix.apk",
apkDir = File("."),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import org.mozilla.tryfox.ui.models.ApksResult
import org.mozilla.tryfox.util.FENIX
import org.mozilla.tryfox.util.FENIX_DEBUG
import org.mozilla.tryfox.util.FENIX_DEBUG_PACKAGE
import org.mozilla.tryfox.util.FENIX_NIGHTLY_PACKAGE
import org.mozilla.tryfox.util.FENIX_RELEASE
import org.mozilla.tryfox.util.FOCUS
import org.mozilla.tryfox.util.FOCUS_RELEASE
Expand Down Expand Up @@ -468,6 +469,44 @@ class HomeViewModelTest {
assertEquals(1, repository.calls)
}

@Test
fun `refresh rereads package state after installed app is removed outside TryFox`() = runTest {
val packageManager = FakeMozillaPackageManager(
mapOf(
FENIX_NIGHTLY_PACKAGE to AppState(
"Fenix",
FENIX_NIGHTLY_PACKAGE,
"125.0a1",
1L,
),
),
)
val repository = CountingReleaseRepository(
testFenixAppName,
NetworkResult.Success(emptyList()),
)
viewModel = createViewModel(
releaseRepositories = listOf(repository),
mozillaPackageManager = packageManager,
)

viewModel.initialLoad()
advanceUntilIdle()
assertEquals(
"125.0a1",
(viewModel.homeScreenState.value as HomeScreenState.Loaded).apps.getValue(FENIX).installedVersion,
)

packageManager.setAppState(AppState("Fenix", FENIX_NIGHTLY_PACKAGE, null, null))
viewModel.refreshInstalledAppStates()
advanceUntilIdle()

assertNull(
(viewModel.homeScreenState.value as HomeScreenState.Loaded).apps.getValue(FENIX).installedVersion,
)
assertFalse(viewModel.isRefreshing.value)
}

@Test
fun `refresh waits for an in-flight load before starting another request`() = runTest {
val repository = BlockingDateReleaseRepository(
Expand Down
Loading