Update module fragment
Update UI and logic for loading modules
This commit is contained in:
parent
c944277e78
commit
f5aa6a3cf8
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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 && viewModel.items.empty}"
|
||||
itemBinding="@{viewModel.itemBinding}"
|
||||
items="@{viewModel.items}"
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
||||
@ -36,4 +36,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</layout>
|
||||
</layout>
|
||||
|
@ -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>
|
||||
|
||||
@ -119,4 +119,4 @@
|
||||
</style>
|
||||
<!--endregion-->
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user