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 {
fun getRepos(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 {
const val LIMIT = 10
}
......
......@@ -95,20 +95,24 @@ class SectionTitle(
) : ObservableItem<SectionTitle>() {
override val layoutRes = R.layout.item_section_md2
@Bindable
var button = _button
@Bindable get
set(value) {
field = value
notifyChange(BR.button)
}
@Bindable
var icon = _icon
@Bindable get
set(value) {
field = value
notifyChange(BR.icon)
}
val hasButton = KObservableField(button != 0 || icon != 0)
var hasButton = button != 0 || icon != 0
@Bindable get
set(value) {
field = value
notifyChange(BR.hasButton)
}
override fun onBindingBound(binding: ViewDataBinding) {
super.onBindingBound(binding)
......@@ -120,14 +124,27 @@ class SectionTitle(
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
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 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 {
......@@ -158,7 +175,7 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
fun delete(viewModel: ModuleViewModel) {
isRemoved = !isRemoved
viewModel.moveToState()
viewModel.updateActiveState()
}
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version
......
......@@ -33,7 +33,6 @@ 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
......@@ -70,13 +69,23 @@ class ModuleViewModel(
}
companion object {
private val sectionRemote =
SectionTitle(R.string.module_section_remote, R.string.sorting_order)
private val sectionRemote = SectionTitle(
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(
R.string.module_section_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton.value = false }
).also { it.hasButton = false }
init {
updateOrderIcon()
......@@ -94,11 +103,13 @@ class ModuleViewModel(
// ---
private val itemsInstalled
@WorkerThread get() = items.asSequence()
.filterIsInstance<ModuleItem>()
.toList()
@WorkerThread get() = items.filterIsInstance<ModuleItem>()
private val itemsUpdatable: List<RepoItem>
@WorkerThread get() = items.filterIsInstance<RepoItem.Update>()
private val itemsRemote
@WorkerThread get() = items.filterIsInstance<RepoItem>()
@WorkerThread get() = items.filterIsInstance<RepoItem.Remote>()
private var remoteJob: Disposable? = null
private val dao
......@@ -126,19 +137,15 @@ class ModuleViewModel(
override fun refresh() = Single.fromCallable { Module.loadModules() }
.map { it.map { ModuleItem(it) } }
.map { it.order() }
.map { build(active = it) }
.map { build(active = it, updatable = loadUpdates(it)) }
.map { it to items.calculateDiff(it) }
.applyViewModel(this)
.subscribeK {
items.update(it.first, it.second)
if (!items.contains(sectionRemote)) {
loadRemote()
} else {
Single.fromCallable { itemsRemote }
.subscribeK { it.ensureUpdateState() }
.add()
}
moveToState()
updateActiveState()
}
fun loadRemoteImplicit() = let { items.clear(); itemsSearch.clear() }
......@@ -181,8 +188,7 @@ class ModuleViewModel(
.subscribeOn(AndroidSchedulers.mainThread())
}
return Single.fromCallable { dao.searchRepos(query, offset) }
.map { it.map { RepoItem(it) } }
.doOnSuccess { it.ensureUpdateState() }
.map { it.map { RepoItem.Remote(it) } }
}
private fun query(query: String = this.query, offset: Int = 0) {
......@@ -207,7 +213,7 @@ class ModuleViewModel(
offset: Int = 0,
downloadRepos: Boolean = offset == 0
): Single<List<RepoItem>> = Single.fromCallable { dao.getRepos(offset) }
.map { it.map { RepoItem(it) } }
.map { it.map { RepoItem.Remote(it) } }
.flatMap {
when {
// 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)
}
}
.doOnSuccess { it.ensureUpdateState() }
private fun downloadRepos() = Single.just(Unit)
.flatMap { repoUpdater() }
......@@ -237,56 +242,15 @@ 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()
@WorkerThread
private fun loadUpdates(installed: List<ModuleItem>) = installed
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
.map { RepoItem.Update(it) }
// ---
fun moveToState() = Single.fromCallable { itemsInstalled.any { it.isModified } }
.subscribeK { sectionActive.hasButton.value = it }
fun updateActiveState() = Single.fromCallable { itemsInstalled.any { it.isModified } }
.subscribeK { sectionActive.hasButton = it }
.add()
fun sectionPressed(item: SectionTitle) = when (item) {
......@@ -318,8 +282,10 @@ class ModuleViewModel(
@WorkerThread
private fun build(
active: List<ModuleItem> = itemsInstalled,
updatable: List<RepoItem> = itemsUpdatable,
remote: List<RepoItem> = itemsRemote
) = (active + InstallModule).prependIfNotEmpty { sectionActive } +
updatable.prependIfNotEmpty { sectionUpdate } +
remote.prependIfNotEmpty { sectionRemote }
private fun <T> List<T>.prependIfNotEmpty(item: () -> T) =
......
......@@ -110,7 +110,7 @@
android:paddingStart="@dimen/l_50"
isEnabled="@{!(item.progress == -100 || (item.progress > 0 &amp;&amp; item.progress &lt; 100))}"
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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider"
......
......@@ -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_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_remote">Remote</string>
<string name="module_state_remove">Remove</string>
......
......@@ -7,7 +7,7 @@ if (configPath.exists())
configPath.withInputStream { is -> props.load(is) }
buildscript {
ext.vKotlin = '1.3.50'
ext.vKotlin = '1.3.60'
repositories {
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