Added checks for updatable state on remote repos

This commit is contained in:
Viktor De Pasquale 2019-11-17 13:46:56 +01:00
parent a7f4496db7
commit 2926772bba
3 changed files with 72 additions and 16 deletions

View File

@ -121,10 +121,10 @@ class SectionTitle(
} }
class RepoItem(val item: Repo) : ComparableRvItem<RepoItem>() { class RepoItem(val item: Repo) : ComparableRvItem<RepoItem>() {
override val layoutRes: Int = R.layout.item_repo_md2 override val layoutRes: Int = R.layout.item_repo_md2
val progress = KObservableField(0) val progress = KObservableField(0)
val isUpdate = KObservableField(false)
override fun contentSameAs(other: RepoItem): Boolean = item == other.item override fun contentSameAs(other: RepoItem): Boolean = item == other.item
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id

View File

@ -33,6 +33,7 @@ import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import org.jetbrains.annotations.NotNull
import timber.log.Timber import timber.log.Timber
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -132,6 +133,10 @@ class ModuleViewModel(
items.update(it.first, it.second) items.update(it.first, it.second)
if (!items.contains(sectionRemote)) { if (!items.contains(sectionRemote)) {
loadRemote() loadRemote()
} else {
Single.fromCallable { itemsRemote }
.subscribeK { it.ensureUpdateState() }
.add()
} }
moveToState() moveToState()
} }
@ -150,10 +155,7 @@ class ModuleViewModel(
} }
remoteJob = Single.fromCallable { itemsRemote.size } remoteJob = Single.fromCallable { itemsRemote.size }
.flatMap { loadRemoteInternal(offset = it) } .flatMap { loadRemoteInternal(offset = it) }
.map { it.map { RepoItem(it) } } .subscribeK(onError = Timber::e) {
.subscribeK(onError = {
Timber.e(it)
}) {
if (!items.contains(sectionRemote)) { if (!items.contains(sectionRemote)) {
items.add(sectionRemote) items.add(sectionRemote)
} }
@ -176,6 +178,7 @@ class ModuleViewModel(
} }
return Single.fromCallable { dao.searchRepos(query, offset) } return Single.fromCallable { dao.searchRepos(query, offset) }
.map { it.map { RepoItem(it) } } .map { it.map { RepoItem(it) } }
.doOnSuccess { it.ensureUpdateState() }
} }
private fun query(query: String = this.query, offset: Int = 0) { private fun query(query: String = this.query, offset: Int = 0) {
@ -199,14 +202,17 @@ class ModuleViewModel(
private fun loadRemoteInternal( private fun loadRemoteInternal(
offset: Int = 0, offset: Int = 0,
downloadRepos: Boolean = offset == 0 downloadRepos: Boolean = offset == 0
): Single<List<Repo>> = Single.fromCallable { dao.getRepos(offset) }.flatMap { ): Single<List<RepoItem>> = Single.fromCallable { dao.getRepos(offset) }
when { .map { it.map { RepoItem(it) } }
// in case we find result empty and offset is initial we need to refresh the repos. .flatMap {
downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos() when {
.andThen(loadRemoteInternal(downloadRepos = false)) // in case we find result empty and offset is initial we need to refresh the repos.
else -> Single.just(it) downloadRepos && it.isEmpty() && offset == 0 -> downloadRepos()
.andThen(loadRemoteInternal(downloadRepos = false))
else -> Single.just(it)
}
} }
} .doOnSuccess { it.ensureUpdateState() }
private fun downloadRepos() = Single.just(Unit) private fun downloadRepos() = Single.just(Unit)
.flatMap { repoUpdater() } .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<ModuleItem>? = 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<RepoItem>.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 } } fun moveToState() = Single.fromCallable { itemsInstalled.any { it.isModified } }
.subscribeK { sectionActive.hasButton.value = it } .subscribeK { sectionActive.hasButton.value = it }
.add() .add()
fun download(item: RepoItem) = ModuleInstallDialog(item.item).publish()
fun sectionPressed(item: SectionTitle) = when (item) { fun sectionPressed(item: SectionTitle) = when (item) {
sectionActive -> reboot() //TODO add reboot picker, regular reboot is not always preferred sectionActive -> reboot() //TODO add reboot picker, regular reboot is not always preferred
sectionRemote -> { sectionRemote -> {
@ -252,6 +304,7 @@ class ModuleViewModel(
else -> Unit else -> Unit
} }
fun downloadPressed(item: RepoItem) = ModuleInstallDialog(item.item).publish()
fun installPressed() = InstallExternalModuleEvent().publish() fun installPressed() = InstallExternalModuleEvent().publish()
fun infoPressed(item: RepoItem) = OpenChangelogEvent(item.item).publish() fun infoPressed(item: RepoItem) = OpenChangelogEvent(item.item).publish()

View File

@ -5,6 +5,8 @@
<data> <data>
<import type="com.topjohnwu.magisk.R" />
<variable <variable
name="item" name="item"
type="com.topjohnwu.magisk.model.entity.recycler.RepoItem" /> type="com.topjohnwu.magisk.model.entity.recycler.RepoItem" />
@ -108,11 +110,12 @@
android:paddingStart="@dimen/l_50" android:paddingStart="@dimen/l_50"
isEnabled="@{!(item.progress == -100 || (item.progress > 0 &amp;&amp; item.progress &lt; 100))}" isEnabled="@{!(item.progress == -100 || (item.progress > 0 &amp;&amp; item.progress &lt; 100))}"
android:contentDescription="@string/download" 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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider" app:layout_constraintTop_toBottomOf="@+id/module_divider"
app:srcCompat="@drawable/ic_download_md2" /> android:onClick="@{() -> viewModel.downloadPressed(item)}"
tools:srcCompat="@drawable/ic_download_md2" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>