Commit 332c1a6c authored by Viktor De Pasquale's avatar Viktor De Pasquale

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
parent 0f1f4305
...@@ -11,6 +11,9 @@ interface RepoBase { ...@@ -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
} }
......
...@@ -95,20 +95,24 @@ class SectionTitle( ...@@ -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( ...@@ -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 { ...@@ -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
......
...@@ -33,7 +33,6 @@ import io.reactivex.Single ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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) =
......
...@@ -110,7 +110,7 @@ ...@@ -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"
......
...@@ -75,7 +75,8 @@ ...@@ -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>
......
...@@ -7,7 +7,7 @@ if (configPath.exists()) ...@@ -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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment