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