Break up RepoDetailsState into repoFlow and archiveStateFlow

Before this commit, the UI would not update, because the `repo` in the `state` was never updated.
This was mostly noticeable when toggling mirrors on and off.

So the UI needs to use repoFlow directly (to listen to RepoManager changes).
But then there is no need for RepoDetailsState anymore.
This commit is contained in:
Thore Goebel
2025-01-16 02:54:05 +01:00
parent 991b301d7f
commit 6ba9f3a77f
3 changed files with 63 additions and 68 deletions

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.MutableCreationExtras
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import org.fdroid.database.Repository
import org.fdroid.download.Mirror
import org.fdroid.fdroid.FDroidApp
import org.fdroid.fdroid.R
@@ -36,15 +37,7 @@ class RepoDetailsActivity : AppCompatActivity() {
private lateinit var viewModel: RepoDetailsViewModel
// Only call this once in onCreate()
private fun initViewModel() {
val repoId = intent.getLongExtra(ARG_REPO_ID, 0)
val repo = FDroidApp.getRepoManager(this).getRepository(repoId)
if (repo == null) {
// repo must have been deleted just now (maybe slow UI?)
finish()
return
}
private fun initViewModel(repo: Repository) {
val factory = RepoDetailsViewModel.Factory
val extras = MutableCreationExtras().apply {
set(RepoDetailsViewModel.APP_KEY, application)
@@ -60,15 +53,34 @@ class RepoDetailsActivity : AppCompatActivity() {
(application as FDroidApp).setSecureWindow(this)
(application as FDroidApp).applyPureBlackBackgroundInDarkTheme(this)
initViewModel()
val repoId = intent.getLongExtra(ARG_REPO_ID, 0)
val repo = FDroidApp.getRepoManager(this).getRepository(repoId)
if (repo == null) {
// repo must have been deleted just now (maybe slow UI?)
finish()
return
}
initViewModel(repo)
// Needs an observer because it is lazy
viewModel.repoLiveData.observe(this, {})
setContent {
val state by viewModel.state.collectAsState()
val repo by viewModel.repoFlow.collectAsState(repo)
val archiveState by viewModel.archiveStateFlow.collectAsState(ArchiveState.UNKNOWN)
val numberOfApps by viewModel.numberAppsFlow.collectAsState(0)
val r = repo
if (r == null) {
finish()
return@setContent
}
FDroidContent {
RepoDetailsScreen(
state = state,
repo = r,
archiveState = archiveState,
numberOfApps = numberOfApps,
// app bar
onBackClicked = { onBackPressedDispatcher.onBackPressed() },
@@ -92,10 +104,10 @@ class RepoDetailsActivity : AppCompatActivity() {
}
private fun onShareClicked() {
val uri = viewModel.state.value.repo.getShareUri()
val repo = viewModel.repoLiveData.value ?: return
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, uri)
putExtra(Intent.EXTRA_TEXT, repo.shareUri)
}
startActivity(
Intent.createChooser(intent, getResources().getString(R.string.share_repository))
@@ -138,7 +150,7 @@ class RepoDetailsActivity : AppCompatActivity() {
}
private fun onShowAppsClicked() {
val repo = viewModel.state.value.repo
val repo = viewModel.repoLiveData.value ?: return
if (!repo.enabled) {
return
}
@@ -149,7 +161,7 @@ class RepoDetailsActivity : AppCompatActivity() {
}
private fun onEditCredentialsClicked() {
val repo = viewModel.state.value.repo
val repo = viewModel.repoLiveData.value ?: return
val view = layoutInflater.inflate(R.layout.login, null, false)
val usernameInput = view.findViewById<TextInputLayout>(R.id.edit_name)
@@ -178,8 +190,8 @@ class RepoDetailsActivity : AppCompatActivity() {
}
private fun onShareMirror(mirror: Mirror) {
val fingerprint = viewModel.state.value.repo.fingerprint
val uri = mirror.getFDroidLinkUrl(fingerprint)
val repo = viewModel.repoLiveData.value ?: return
val uri = mirror.getFDroidLinkUrl(repo.fingerprint)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, uri)

View File

@@ -63,7 +63,8 @@ import org.fdroid.fdroid.compose.FDroidSwitchRow
@Composable
fun RepoDetailsScreen(
state: RepoDetailsState,
repo: Repository,
archiveState: ArchiveState,
numberOfApps: Int,
// app bar functions
onBackClicked: () -> Unit,
@@ -80,9 +81,9 @@ fun RepoDetailsScreen(
onShareMirror: (Mirror) -> Unit,
onDeleteMirror: (Mirror) -> Unit,
) {
val officialMirrors = state.repo.getAllOfficialMirrors()
val userMirrors = state.repo.getAllUserMirrors()
val disabledMirrors = state.repo.disabledMirrors.toHashSet()
val officialMirrors = repo.getAllOfficialMirrors()
val userMirrors = repo.getAllUserMirrors()
val disabledMirrors = repo.disabledMirrors.toHashSet()
Scaffold(topBar = {
TopAppBar(elevation = 4.dp,
@@ -125,17 +126,17 @@ fun RepoDetailsScreen(
) {
Spacer(modifier = Modifier) // spacedBy will provide the padding
GeneralInfoCard(
state.repo,
state.archiveState,
repo,
archiveState,
numberOfApps,
onShowAppsClicked,
onToggleArchiveClicked,
)
BasicAuthCard(state.repo, onEditCredentialsClicked)
if (state.repo.certificate.isEmpty()) {
BasicAuthCard(repo, onEditCredentialsClicked)
if (repo.certificate.isEmpty()) {
UnsignedCard()
} else {
FingerprintCard(state.repo)
FingerprintExpandable(repo)
}
// The repo's address is currently also an official mirror.
// So if there is only one mirror, this is the address => don't show this section.
@@ -423,7 +424,8 @@ fun RepoDetailsScreenPreview() {
val repo = FDroidApp.createSwapRepo("https://example.org/fdroid/repo", "foo bar")
FDroidContent {
RepoDetailsScreen(
RepoDetailsState(repo, ArchiveState.ENABLED),
repo,
ArchiveState.ENABLED,
numberOfApps = 42,
{}, {}, {}, {}, {}, // app bar
{}, {}, {}, // other buttons

View File

@@ -17,8 +17,6 @@ import info.guardianproject.netcipher.NetCipher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -32,11 +30,6 @@ import org.fdroid.fdroid.data.DBHelper
import org.fdroid.fdroid.generateQrBitmapKt
import org.fdroid.fdroid.work.RepoUpdateWorker
data class RepoDetailsState(
val repo: Repository,
val archiveState: ArchiveState,
)
enum class ArchiveState {
ENABLED,
DISABLED,
@@ -45,10 +38,12 @@ enum class ArchiveState {
class RepoDetailsViewModel(
app: Application,
initialRepo: Repository,
private val initialRepo: Repository,
) : AndroidViewModel(app) {
companion object {
private const val TAG = "RepoDetailsViewModel"
// TODO: Use androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
// That seems to require setting up dependency injection.
val APP_KEY = object : CreationExtras.Key<Application> {}
@@ -62,19 +57,15 @@ class RepoDetailsViewModel(
}
}
private val repoId = initialRepo.repoId
private val repoManager = FDroidApp.getRepoManager(app)
private val repositoryDao = DBHelper.getDb(app).getRepositoryDao()
private val appDao = DBHelper.getDb(app).getAppDao()
private val _state = MutableStateFlow(
RepoDetailsState(initialRepo, initialRepo.archiveState())
)
val state = _state.asStateFlow()
val liveData = _state.asLiveData()
val repoFlow = combine(_state, repoManager.repositoriesState) { s, reposState ->
reposState.find { repo -> repo.repoId == s.repo.repoId }
}.distinctUntilChanged()
val repoFlow: Flow<Repository?> = repoManager.repositoriesState.map { reposState ->
reposState.find { repo -> repo.repoId == repoId }
}
val repoLiveData = repoFlow.asLiveData()
val numberAppsFlow: Flow<Int> = repoFlow.map { repo ->
@@ -82,23 +73,24 @@ class RepoDetailsViewModel(
appDao.getNumberOfAppsInRepository(repo.repoId)
} else 0
}.flowOn(Dispatchers.IO).distinctUntilChanged()
val numberOfAppsLiveData = numberAppsFlow.asLiveData()
val archiveStateFlow = MutableStateFlow(initialRepo.archiveState())
val qrCodeLiveData = MutableLiveData<Bitmap?>(null)
fun setArchiveRepoEnabled(enabled: Boolean) {
val repo = _state.value.repo
_state.value = _state.value.copy(archiveState = ArchiveState.UNKNOWN)
viewModelScope.launch(Dispatchers.IO) {
val repo = repoLiveData.value ?: return@launch
archiveStateFlow.emit(ArchiveState.UNKNOWN)
try {
val repoId = repoManager.setArchiveRepoEnabled(repo, enabled, NetCipher.getProxy())
_state.value = _state.value.copy(archiveState = enabled.toArchiveState())
archiveStateFlow.emit(enabled.toArchiveState())
if (enabled && repoId != null) withContext(Dispatchers.Main) {
RepoUpdateWorker.updateNow(getApplication(), repoId)
}
} catch (e: Exception) {
Log.e(this.javaClass.simpleName, "Error toggling archive repo: ", e)
_state.value = _state.value.copy(archiveState = repo.archiveState())
Log.e(TAG, "Error toggling archive repo: ", e)
archiveStateFlow.emit(repo.archiveState())
withContext(Dispatchers.Main) {
Toast.makeText(getApplication(), R.string.repo_archive_failed, LENGTH_SHORT)
.show()
@@ -108,35 +100,24 @@ class RepoDetailsViewModel(
}
fun deleteRepository() {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
repoManager.deleteRepository(repoId)
}
}
fun updateUsernameAndPassword(username: String, password: String) {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
repositoryDao.updateUsernameAndPassword(repoId, username, password)
}
}
fun updateDisabledMirrors(toDisable: List<String>) {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
repositoryDao.updateDisabledMirrors(repoId, toDisable)
}
}
fun setMirrorEnabled(mirror: Mirror, enabled: Boolean) {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
repoManager.setMirrorEnabled(repoId, mirror, enabled)
}
}
fun deleteUserMirror(mirror: Mirror) {
val repoId = _state.value.repo.repoId
viewModelScope.launch(Dispatchers.IO) {
repoManager.deleteUserMirror(repoId, mirror)
}
@@ -159,13 +140,13 @@ class RepoDetailsViewModel(
// TODO: initialise this once on ViewModel creation, and don't take an Activity, do fixed size
fun generateQrCode(activity: AppCompatActivity) {
val repo = _state.value.repo
if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) {
// no need to show a QR Code, it is not shareable
qrCodeLiveData.value = null
return
}
viewModelScope.launch(Dispatchers.Default) {
val repo = repoLiveData.value ?: return@launch
if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) {
// no need to show a QR Code, it is not shareable
qrCodeLiveData.value = null
return@launch
}
val bitmap = generateQrBitmapKt(activity, repo.shareUri)
withContext(Dispatchers.Main) {
qrCodeLiveData.value = bitmap