Removed overcomplicated updates loading

The mechanism was replaced by loading updated directly by id to the initial list. There are two factors why yesterday-me was dumb:

1) By asynchronously loading update state, you have no control over it - hence no search
2) It's incredibly wasteful; running that hardcore search on every query? Not cool
...and from UX stand-point having updates inlined right under installed modules is by far better than nitpicking it from the list or in the search
This commit is contained in:
Viktor De Pasquale 2019-11-18 17:20:54 +01:00
parent 0f1f43057e
commit 332c1a6c59
6 changed files with 62 additions and 75 deletions

View File

@ -11,6 +11,9 @@ interface RepoBase {
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo> fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo> fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
companion object { companion object {
const val LIMIT = 10 const val LIMIT = 10
} }

View File

@ -95,20 +95,24 @@ class SectionTitle(
) : ObservableItem<SectionTitle>() { ) : ObservableItem<SectionTitle>() {
override val layoutRes = R.layout.item_section_md2 override val layoutRes = R.layout.item_section_md2
@Bindable
var button = _button var button = _button
@Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.button) notifyChange(BR.button)
} }
@Bindable
var icon = _icon var icon = _icon
@Bindable get
set(value) { set(value) {
field = value field = value
notifyChange(BR.icon) notifyChange(BR.icon)
} }
var hasButton = button != 0 || icon != 0
val hasButton = KObservableField(button != 0 || icon != 0) @Bindable get
set(value) {
field = value
notifyChange(BR.hasButton)
}
override fun onBindingBound(binding: ViewDataBinding) { override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding) super.onBindingBound(binding)
@ -120,14 +124,27 @@ class SectionTitle(
override fun contentSameAs(other: SectionTitle): Boolean = this === other override fun contentSameAs(other: SectionTitle): Boolean = this === other
} }
class RepoItem(val item: Repo) : ComparableRvItem<RepoItem>() { sealed class RepoItem(val item: Repo) : ObservableItem<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) var isUpdate = false
@Bindable get
protected set(value) {
field = value
notifyChange(BR.update)
}
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
class Update(item: Repo) : RepoItem(item) {
init {
isUpdate = true
}
}
class Remote(item: Repo) : RepoItem(item)
} }
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable { class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
@ -158,7 +175,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
fun delete(viewModel: ModuleViewModel) { fun delete(viewModel: ModuleViewModel) {
isRemoved = !isRemoved isRemoved = !isRemoved
viewModel.moveToState() viewModel.updateActiveState()
} }
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version

View File

@ -33,7 +33,6 @@ 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
@ -70,13 +69,23 @@ class ModuleViewModel(
} }
companion object { companion object {
private val sectionRemote = private val sectionRemote = SectionTitle(
SectionTitle(R.string.module_section_remote, R.string.sorting_order) R.string.module_section_remote,
R.string.sorting_order
)
private val sectionUpdate = SectionTitle(
R.string.module_section_pending,
R.string.module_section_pending_action,
R.drawable.ic_update_md2
// enable with implementation of https://github.com/topjohnwu/Magisk/issues/2036
).also { it.hasButton = false }
private val sectionActive = SectionTitle( private val sectionActive = SectionTitle(
R.string.module_section_installed, R.string.module_section_installed,
R.string.reboot, R.string.reboot,
R.drawable.ic_restart R.drawable.ic_restart
).also { it.hasButton.value = false } ).also { it.hasButton = false }
init { init {
updateOrderIcon() updateOrderIcon()
@ -94,11 +103,13 @@ class ModuleViewModel(
// --- // ---
private val itemsInstalled private val itemsInstalled
@WorkerThread get() = items.asSequence() @WorkerThread get() = items.filterIsInstance<ModuleItem>()
.filterIsInstance<ModuleItem>()
.toList() private val itemsUpdatable: List<RepoItem>
@WorkerThread get() = items.filterIsInstance<RepoItem.Update>()
private val itemsRemote private val itemsRemote
@WorkerThread get() = items.filterIsInstance<RepoItem>() @WorkerThread get() = items.filterIsInstance<RepoItem.Remote>()
private var remoteJob: Disposable? = null private var remoteJob: Disposable? = null
private val dao private val dao
@ -126,19 +137,15 @@ class ModuleViewModel(
override fun refresh() = Single.fromCallable { Module.loadModules() } override fun refresh() = Single.fromCallable { Module.loadModules() }
.map { it.map { ModuleItem(it) } } .map { it.map { ModuleItem(it) } }
.map { it.order() } .map { it.order() }
.map { build(active = it) } .map { build(active = it, updatable = loadUpdates(it)) }
.map { it to items.calculateDiff(it) } .map { it to items.calculateDiff(it) }
.applyViewModel(this) .applyViewModel(this)
.subscribeK { .subscribeK {
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() updateActiveState()
} }
fun loadRemoteImplicit() = let { items.clear(); itemsSearch.clear() } fun loadRemoteImplicit() = let { items.clear(); itemsSearch.clear() }
@ -181,8 +188,7 @@ class ModuleViewModel(
.subscribeOn(AndroidSchedulers.mainThread()) .subscribeOn(AndroidSchedulers.mainThread())
} }
return Single.fromCallable { dao.searchRepos(query, offset) } return Single.fromCallable { dao.searchRepos(query, offset) }
.map { it.map { RepoItem(it) } } .map { it.map { RepoItem.Remote(it) } }
.doOnSuccess { it.ensureUpdateState() }
} }
private fun query(query: String = this.query, offset: Int = 0) { private fun query(query: String = this.query, offset: Int = 0) {
@ -207,7 +213,7 @@ class ModuleViewModel(
offset: Int = 0, offset: Int = 0,
downloadRepos: Boolean = offset == 0 downloadRepos: Boolean = offset == 0
): Single<List<RepoItem>> = Single.fromCallable { dao.getRepos(offset) } ): Single<List<RepoItem>> = Single.fromCallable { dao.getRepos(offset) }
.map { it.map { RepoItem(it) } } .map { it.map { RepoItem.Remote(it) } }
.flatMap { .flatMap {
when { when {
// in case we find result empty and offset is initial we need to refresh the repos. // in case we find result empty and offset is initial we need to refresh the repos.
@ -216,7 +222,6 @@ class ModuleViewModel(
else -> Single.just(it) else -> Single.just(it)
} }
} }
.doOnSuccess { it.ensureUpdateState() }
private fun downloadRepos() = Single.just(Unit) private fun downloadRepos() = Single.just(Unit)
.flatMap { repoUpdater() } .flatMap { repoUpdater() }
@ -237,56 +242,15 @@ class ModuleViewModel(
// --- // ---
/** @WorkerThread
* Dynamically allocated list of [itemsInstalled]. It might be invalidated any time on any private fun loadUpdates(installed: List<ModuleItem>) = installed
* thread hence it needs to be volatile. .mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
* .map { RepoItem.Update(it) }
* 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 updateActiveState() = Single.fromCallable { itemsInstalled.any { it.isModified } }
.subscribeK { sectionActive.hasButton.value = it } .subscribeK { sectionActive.hasButton = it }
.add() .add()
fun sectionPressed(item: SectionTitle) = when (item) { fun sectionPressed(item: SectionTitle) = when (item) {
@ -318,8 +282,10 @@ class ModuleViewModel(
@WorkerThread @WorkerThread
private fun build( private fun build(
active: List<ModuleItem> = itemsInstalled, active: List<ModuleItem> = itemsInstalled,
updatable: List<RepoItem> = itemsUpdatable,
remote: List<RepoItem> = itemsRemote remote: List<RepoItem> = itemsRemote
) = (active + InstallModule).prependIfNotEmpty { sectionActive } + ) = (active + InstallModule).prependIfNotEmpty { sectionActive } +
updatable.prependIfNotEmpty { sectionUpdate } +
remote.prependIfNotEmpty { sectionRemote } remote.prependIfNotEmpty { sectionRemote }
private fun <T> List<T>.prependIfNotEmpty(item: () -> T) = private fun <T> List<T>.prependIfNotEmpty(item: () -> T) =

View File

@ -110,7 +110,7 @@
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"
srcCompat="@{item.isUpdate() ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}" 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"

View File

@ -75,7 +75,8 @@
<string name="module_safe_mode_message">You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled.</string> <string name="module_safe_mode_message">You\'re in safe mode. None of user modules will work.\nThis message will disappear once safe mode is disabled.</string>
<string name="module_version_author">%1$s by %2$s</string> <string name="module_version_author">%1$s by %2$s</string>
<string name="module_section_pending">Pending changes</string> <string name="module_section_pending">Updates</string>
<string name="module_section_pending_action">Update all</string>
<string name="module_section_installed">Installed</string> <string name="module_section_installed">Installed</string>
<string name="module_section_remote">Remote</string> <string name="module_section_remote">Remote</string>
<string name="module_state_remove">Remove</string> <string name="module_state_remove">Remove</string>

View File

@ -7,7 +7,7 @@ if (configPath.exists())
configPath.withInputStream { is -> props.load(is) } configPath.withInputStream { is -> props.load(is) }
buildscript { buildscript {
ext.vKotlin = '1.3.50' ext.vKotlin = '1.3.60'
repositories { repositories {
google() google()