Update module fragment

Update UI and logic for loading modules
This commit is contained in:
topjohnwu 2020-08-09 21:41:23 -07:00
parent c944277e78
commit f5aa6a3cf8
8 changed files with 136 additions and 155 deletions

View File

@ -92,7 +92,7 @@ class RepoUpdater(
}
}
suspend operator fun invoke(forced: Boolean) = withContext(Dispatchers.IO) {
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
val cached = HashSet(repoDB.repoIDList).synchronized()
when (loadPage(cached, etag = repoDB.etagKey)) {
PageResult.CACHED -> if (forced) forcedReload(cached)

View File

@ -1,7 +1,6 @@
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.Bindable
import androidx.databinding.Observable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.module.Module
@ -62,7 +61,7 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
}
}
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>() {
override val layoutRes = R.layout.item_module_md2
@ -71,19 +70,15 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
set(value) = set(value, field, { field = it }, BR.repo)
@get:Bindable
var isEnabled
get() = item.enable
set(value) {
var isEnabled = item.enable
set(value) = set(value, field, { field = it }, BR.enabled) {
item.enable = value
notifyPropertyChanged(BR.enabled)
}
@get:Bindable
var isRemoved
get() = item.remove
set(value) {
var isRemoved = item.remove
set(value) = set(value, field, { field = it }, BR.removed) {
item.remove = value
notifyPropertyChanged(BR.removed)
}
val isUpdated get() = item.updated

View File

@ -27,7 +27,7 @@ inline fun <T : ComparableRvItem<*>> filterableListOf(
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems.toList()) }
fun <T : ComparableRvItem<*>> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,

View File

@ -2,28 +2,34 @@ package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.download.RemoteFileService
import com.topjohnwu.magisk.core.model.module.Module
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoByNameDao
import com.topjohnwu.magisk.data.database.RepoByUpdatedDao
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.ktx.addOnListChangedCallback
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.recycler.*
import com.topjohnwu.magisk.model.entity.recycler.InstallModule
import com.topjohnwu.magisk.model.entity.recycler.ModuleItem
import com.topjohnwu.magisk.model.entity.recycler.RepoItem
import com.topjohnwu.magisk.model.entity.recycler.SectionTitle
import com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ui.base.*
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.set
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import kotlin.math.roundToInt
@ -45,7 +51,7 @@ class ModuleViewModel(
private val repoName: RepoByNameDao,
private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater
) : BaseViewModel(), Queryable {
) : BaseViewModel(), Queryable, Observer<Pair<Float, DownloadSubject>> {
override val queryDelay = 1000L
private var queryJob: Job? = null
@ -72,63 +78,44 @@ class ModuleViewModel(
it.bindExtra(BR.viewModel, this)
}
private val itemNoneInstalled = TextItem(R.string.no_modules_found)
private val itemNoneUpdatable = TextItem(R.string.module_update_none)
private val itemsInstalledHelpers = ObservableArrayList<TextItem>()
private val itemsUpdatableHelpers = ObservableArrayList<TextItem>()
private val installSectionList = ObservableArrayList<RvItem>()
private val updatableSectionList = ObservableArrayList<RvItem>()
private val itemsInstalled = diffListOf<ModuleItem>()
private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsRemote = diffListOf<RepoItem.Remote>()
val adapter = adapterOf<ComparableRvItem<*>>()
val items = MergeObservableList<ComparableRvItem<*>>()
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 sectionInstalled = SectionTitle(
R.string.module_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
private val sectionRemote = SectionTitle(
R.string.module_section_remote,
R.string.sorting_order
).apply { updateOrderIcon() }
val adapter = adapterOf<RvItem>()
val items = MergeObservableList<RvItem>()
.insertItem(InstallModule)
.insertItem(sectionUpdate)
.insertList(itemsUpdatableHelpers)
.insertList(updatableSectionList)
.insertList(itemsUpdatable)
.insertItem(sectionActive)
.insertList(itemsInstalledHelpers)
.insertList(installSectionList)
.insertList(itemsInstalled)
.insertItem(sectionRemote)
.insertList(itemsRemote)!!
val itemBinding = itemBindingOf<ComparableRvItem<*>> {
val itemBinding = itemBindingOf<RvItem> {
it.bindExtra(BR.viewModel, this)
}
companion object {
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_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
init {
updateOrderIcon()
}
private fun updateOrderIcon() {
sectionRemote.icon = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> R.drawable.ic_order_name
Config.Value.ORDER_DATE -> R.drawable.ic_order_date
else -> return
}
}
}
// ---
private var refetch = false
@ -143,63 +130,92 @@ class ModuleViewModel(
init {
RemoteFileService.reset()
RemoteFileService.progressBroadcast.observeForever {
val (progress, subject) = it ?: return@observeForever
if (subject !is DownloadSubject.Module) {
return@observeForever
}
update(subject.module, progress.times(100).roundToInt())
}
RemoteFileService.progressBroadcast.observeForever(this)
itemsInstalled.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ -> itemsInstalledHelpers.clear() },
onItemRangeRemoved = { _, _, _ -> addInstalledEmptyMessage() }
onItemRangeInserted = { _, _, _ ->
if (installSectionList.isEmpty())
installSectionList.add(sectionInstalled)
},
onItemRangeRemoved = { list, _, _ ->
if (list.isEmpty())
installSectionList.clear()
}
)
itemsUpdatable.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ -> itemsUpdatableHelpers.clear() },
onItemRangeRemoved = { _, _, _ -> addUpdatableEmptyMessage() }
onItemRangeInserted = { _, _, _ ->
if (updatableSectionList.isEmpty())
updatableSectionList.add(sectionUpdate)
},
onItemRangeRemoved = { list, _, _ ->
if (list.isEmpty())
updatableSectionList.clear()
}
)
}
// ---
override fun refresh(): Job {
if (itemsRemote.isEmpty())
loadRemote()
return loadInstalled()
override fun onCleared() {
super.onCleared()
RemoteFileService.progressBroadcast.removeObserver(this)
}
private suspend fun loadUpdates(installed: List<ModuleItem>) = withContext(Dispatchers.IO) {
installed
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
.map { RepoItem.Update(it) }
}
override fun onChanged(it: Pair<Float, DownloadSubject>?) {
val (progress, subject) = it ?: return
if (subject !is DownloadSubject.Module)
return
private suspend fun List<ModuleItem>.loadDetails() = withContext(Dispatchers.IO) {
onEach {
launch {
it.repo = dao.getRepoById(it.item.id)
viewModelScope.launch {
val items = withContext(Dispatchers.Default) {
val predicate = { it: RepoItem -> it.item.id == subject.module.id }
itemsUpdatable.filter(predicate) +
itemsRemote.filter(predicate) +
itemsSearch.filter(predicate)
}
items.forEach { it.progress = progress.times(100).roundToInt() }
}
}
private fun loadInstalled() = viewModelScope.launch {
state = State.LOADING
val installed = Module.installed().map { ModuleItem(it) }
val detailLoad = async { installed.loadDetails() }
val updates = loadUpdates(installed)
val diff = withContext(Dispatchers.Default) {
val i = async { itemsInstalled.calculateDiff(installed) }
val u = async { itemsUpdatable.calculateDiff(updates) }
awaitAll(i, u)
override fun refresh(): Job {
return viewModelScope.launch {
loadInstalled()
if (itemsRemote.isEmpty())
loadRemote()
}
detailLoad.await()
itemsInstalled.update(installed, diff[0])
itemsUpdatable.update(updates, diff[1])
addInstalledEmptyMessage()
addUpdatableEmptyMessage()
updateActiveState()
state = State.LOADED
}
private fun SectionTitle.updateOrderIcon() {
hasButton = true
icon = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> R.drawable.ic_order_name
Config.Value.ORDER_DATE -> R.drawable.ic_order_date
else -> return
}
}
private suspend fun loadInstalled() {
val installed = Module.installed().map { ModuleItem(it) }
val diff = withContext(Dispatchers.Default) {
itemsInstalled.calculateDiff(installed)
}
itemsInstalled.update(installed, diff)
}
private suspend fun loadUpdatable() {
val (updates, diff) = withContext(Dispatchers.IO) {
itemsInstalled.forEach {
launch {
it.repo = dao.getRepoById(it.item.id)
}
}
val updates = itemsInstalled
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
.map { RepoItem.Update(it) }
val diff = itemsUpdatable.calculateDiff(updates)
return@withContext updates to diff
}
itemsUpdatable.update(updates, diff)
}
fun loadRemote() {
@ -217,7 +233,8 @@ class ModuleViewModel(
isRemoteLoading = true
val repos = if (itemsRemote.isEmpty()) {
repoUpdater(refetch)
repoUpdater.run(refetch)
loadUpdatable()
loadRemoteDB(0)
} else {
loadRemoteDB(itemsRemote.size)
@ -230,6 +247,7 @@ class ModuleViewModel(
fun forceRefresh() {
itemsRemote.clear()
itemsUpdatable.clear()
itemsSearch.clear()
refetch = true
refresh()
@ -271,47 +289,21 @@ class ModuleViewModel(
// ---
private fun update(repo: Repo, progress: Int) = viewModelScope.launch {
val items = withContext(Dispatchers.Default) {
val predicate = { it: RepoItem -> it.item.id == repo.id }
itemsUpdatable.filter(predicate) +
itemsRemote.filter(predicate) +
itemsSearch.filter(predicate)
}
items.forEach { it.progress = progress }
}
// ---
private fun addInstalledEmptyMessage() {
if (itemsInstalled.isEmpty() && itemsInstalledHelpers.isEmpty()) {
itemsInstalledHelpers.add(itemNoneInstalled)
}
}
private fun addUpdatableEmptyMessage() {
if (itemsUpdatable.isEmpty() && itemsUpdatableHelpers.isEmpty()) {
itemsUpdatableHelpers.add(itemNoneUpdatable)
}
}
// ---
fun updateActiveState() = viewModelScope.launch {
sectionActive.hasButton = withContext(Dispatchers.Default) {
sectionInstalled.hasButton = withContext(Dispatchers.Default) {
itemsInstalled.any { it.isModified }
}
}
fun sectionPressed(item: SectionTitle) = when (item) {
sectionActive -> reboot() // TODO add reboot picker, regular reboot is not always preferred
sectionInstalled -> reboot() // TODO add reboot picker, regular reboot is not always preferred
sectionRemote -> {
Config.repoOrder = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> Config.Value.ORDER_DATE
Config.Value.ORDER_DATE -> Config.Value.ORDER_NAME
else -> Config.Value.ORDER_NAME
}
updateOrderIcon()
sectionRemote.updateOrderIcon()
queryHandler.post {
itemsRemote.clear()
loadRemote()

View File

@ -32,7 +32,7 @@
android:id="@+id/module_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l_50}"
gone="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"

View File

@ -15,42 +15,36 @@
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:layout_height="wrap_content">
android:gravity="center_vertical">
<androidx.appcompat.widget.AppCompatTextView
<TextView
android:id="@+id/module_title"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textAppearance="@style/AppearanceFoundation.Large"
android:textColor="@color/color_primary_transient"
android:textStyle="bold"
tools:text="@tools:sample/lorem/random"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/module_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
tools:text="Installed" />
<com.google.android.material.button.MaterialButton
android:id="@+id/module_button"
style="@style/WidgetFoundation.Button.Text.Secondary"
gone="@{!item.hasButton}"
invisible="@{!item.hasButton}"
android:onClick="@{() -> viewModel.sectionPressed(item)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:text="@{item.button}"
tools:text="@tools:sample/lorem"
android:textAllCaps="false"
app:icon="@{item.icon}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/module_title"
app:layout_constraintTop_toTopOf="parent" />
tools:text="Reboot" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</layout>

View File

@ -22,7 +22,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Display.Secondary"
android:textAppearance="@style/AppearanceFoundation.Large.Secondary"
android:textStyle="bold"
tools:text="@tools:sample/lorem" />

View File

@ -2,19 +2,19 @@
<resources>
<!--region Display-->
<style name="AppearanceFoundation.Display" parent="TextAppearance.AppCompat.Display1">
<style name="AppearanceFoundation.Large" parent="TextAppearance.AppCompat.Large">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="AppearanceFoundation.Display.Secondary">
<style name="AppearanceFoundation.Large.Secondary">
<item name="android:textColor">?colorSecondary</item>
</style>
<style name="AppearanceFoundation.Display.Variant">
<style name="AppearanceFoundation.Large.Variant">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
<style name="AppearanceFoundation.Display.OnPrimary">
<style name="AppearanceFoundation.Display.OnPrimary" parent="TextAppearance.AppCompat.Display1">
<item name="android:textColor">?attr/colorOnPrimary</item>
</style>