diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt index 657c144ab..8a76ee431 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -121,10 +121,10 @@ class SectionTitle( } class RepoItem(val item: Repo) : ComparableRvItem() { - override val layoutRes: Int = R.layout.item_repo_md2 val progress = KObservableField(0) + val isUpdate = KObservableField(false) override fun contentSameAs(other: RepoItem): Boolean = item == other.item override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt index 52d681208..320bf8357 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/module/ModuleViewModel.kt @@ -33,6 +33,7 @@ import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter +import org.jetbrains.annotations.NotNull import timber.log.Timber import kotlin.math.roundToInt @@ -132,6 +133,10 @@ class ModuleViewModel( items.update(it.first, it.second) if (!items.contains(sectionRemote)) { loadRemote() + } else { + Single.fromCallable { itemsRemote } + .subscribeK { it.ensureUpdateState() } + .add() } moveToState() } @@ -150,10 +155,7 @@ class ModuleViewModel( } remoteJob = Single.fromCallable { itemsRemote.size } .flatMap { loadRemoteInternal(offset = it) } - .map { it.map { RepoItem(it) } } - .subscribeK(onError = { - Timber.e(it) - }) { + .subscribeK(onError = Timber::e) { if (!items.contains(sectionRemote)) { items.add(sectionRemote) } @@ -176,6 +178,7 @@ class ModuleViewModel( } return Single.fromCallable { dao.searchRepos(query, offset) } .map { it.map { RepoItem(it) } } + .doOnSuccess { it.ensureUpdateState() } } private fun query(query: String = this.query, offset: Int = 0) { @@ -199,14 +202,17 @@ class ModuleViewModel( private fun loadRemoteInternal( offset: Int = 0, downloadRepos: Boolean = offset == 0 - ): Single> = Single.fromCallable { dao.getRepos(offset) }.flatMap { - when { - // in case we find result empty and offset is initial we need to refresh the repos. - downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos() - .andThen(loadRemoteInternal(downloadRepos = false)) - else -> Single.just(it) + ): Single> = Single.fromCallable { dao.getRepos(offset) } + .map { it.map { RepoItem(it) } } + .flatMap { + when { + // in case we find result empty and offset is initial we need to refresh the repos. + downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos() + .andThen(loadRemoteInternal(downloadRepos = false)) + else -> Single.just(it) + } } - } + .doOnSuccess { it.ensureUpdateState() } private fun downloadRepos() = Single.just(Unit) .flatMap { repoUpdater() } @@ -227,12 +233,58 @@ class ModuleViewModel( // --- + /** + * Dynamically allocated list of [itemsInstalled]. It might be invalidated any time on any + * thread hence it needs to be volatile. + * + * There might be a state where this field gets assigned `null` whilst being used by another + * instance of any job, so the list will be immediately reinstated back. + * + * ### Note: + * + * It is caller's responsibility to invalidate this variable at the end of every job to save + * memory. + * */ + @Volatile + private var cachedItemsInstalled: List? = null + @WorkerThread @NotNull get() = field ?: itemsInstalled.also { field = it } + + private val Repo.isUpdatable: Boolean + @WorkerThread get() { + val installed = cachedItemsInstalled!! + .firstOrNull { it.item.id == id } + ?: return false + return installed.item.versionCode < versionCode + } + + /** + * Asynchronously updates state of all repo items so the loading speed is not impaired by this + * seemingly unnecessary operation. Because of the nature of this operation, the "update" status + * is not guaranteed for all items and can change any time. + * + * It is permitted running this function in parallel; it will also attempt to run in parallel + * by itself to finish the job as quickly as possible. + * + * No list manipulations should be done in this method whatsoever! By being heavily parallelized + * is will inevitably throw exceptions by simultaneously accessing the same list. + * + * In order to save time it uses helper [cachedItemsInstalled]. + * */ + private fun List.ensureUpdateState() = Single.just(this) + .flattenAsFlowable { it } + .parallel() + .map { it to it.item.isUpdatable } + .sequential() + .doOnComplete { cachedItemsInstalled = null } + .subscribeK { it.first.isUpdate.value = it.second } + .add() + + // --- + fun moveToState() = Single.fromCallable { itemsInstalled.any { it.isModified } } .subscribeK { sectionActive.hasButton.value = it } .add() - fun download(item: RepoItem) = ModuleInstallDialog(item.item).publish() - fun sectionPressed(item: SectionTitle) = when (item) { sectionActive -> reboot() //TODO add reboot picker, regular reboot is not always preferred sectionRemote -> { @@ -252,6 +304,7 @@ class ModuleViewModel( else -> Unit } + fun downloadPressed(item: RepoItem) = ModuleInstallDialog(item.item).publish() fun installPressed() = InstallExternalModuleEvent().publish() fun infoPressed(item: RepoItem) = OpenChangelogEvent(item.item).publish() diff --git a/app/src/main/res/layout/item_repo_md2.xml b/app/src/main/res/layout/item_repo_md2.xml index 19093ef87..3c76aed55 100644 --- a/app/src/main/res/layout/item_repo_md2.xml +++ b/app/src/main/res/layout/item_repo_md2.xml @@ -5,6 +5,8 @@ + + @@ -108,11 +110,12 @@ android:paddingStart="@dimen/l_50" isEnabled="@{!(item.progress == -100 || (item.progress > 0 && item.progress < 100))}" android:contentDescription="@string/download" - android:onClick="@{() -> viewModel.download(item)}" + srcCompat="@{item.isUpdate() ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/module_divider" - app:srcCompat="@drawable/ic_download_md2" /> + android:onClick="@{() -> viewModel.downloadPressed(item)}" + tools:srcCompat="@drawable/ic_download_md2" />