From 6ba9f3a77f29977db29780ce660d300afbca2c8f Mon Sep 17 00:00:00 2001 From: Thore Goebel Date: Thu, 16 Jan 2025 02:54:05 +0100 Subject: [PATCH] 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. --- .../fdroid/views/repos/RepoDetailsActivity.kt | 48 +++++++++------ .../fdroid/views/repos/RepoDetailsScreen.kt | 22 ++++--- .../views/repos/RepoDetailsViewModel.kt | 61 +++++++------------ 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt index 155a29d11..f25ffd954 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.kt @@ -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(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) diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt index b12bf19d8..4c25733fe 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsScreen.kt @@ -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 diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt index 29a2ffaf4..0e7bdc6b3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt @@ -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 {} @@ -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 = repoManager.repositoriesState.map { reposState -> + reposState.find { repo -> repo.repoId == repoId } + } val repoLiveData = repoFlow.asLiveData() val numberAppsFlow: Flow = 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(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) { - 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