From f5aa6a3cf8d2f68f6c8b45aff4032a39c192c0ca Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Sun, 9 Aug 2020 21:41:23 -0700 Subject: [PATCH] Update module fragment Update UI and logic for loading modules --- .../magisk/core/tasks/RepoUpdater.kt | 2 +- .../model/entity/recycler/ModuleRvItem.kt | 15 +- .../topjohnwu/magisk/ui/base/CompatHelpers.kt | 2 +- .../magisk/ui/module/ModuleViewModel.kt | 230 +++++++++--------- .../main/res/layout/fragment_module_md2.xml | 2 +- app/src/main/res/layout/item_section_md2.xml | 26 +- .../main/res/layout/item_settings_section.xml | 4 +- .../main/res/values/styles_md2_appearance.xml | 10 +- 8 files changed, 136 insertions(+), 155 deletions(-) diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt index c8691483a..d66ecfdc4 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt @@ -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) diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt index c412384c5..59415e1d5 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -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() { } } -class ModuleItem(val item: Module) : ObservableItem(), Observable { +class ModuleItem(val item: Module) : ObservableItem() { override val layoutRes = R.layout.item_module_md2 @@ -71,19 +70,15 @@ class ModuleItem(val item: Module) : ObservableItem(), 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt index 82eca1c3c..4582007c8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt @@ -27,7 +27,7 @@ inline fun > filterableListOf( override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem) }).also { it.update(newItems.toList()) } -fun > adapterOf() = object : BindingRecyclerViewAdapter() { +fun adapterOf() = object : BindingRecyclerViewAdapter() { override fun onBindBinding( binding: ViewDataBinding, variableId: Int, diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 5c065ac3c..f7ec4626a 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -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> { 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() - private val itemsUpdatableHelpers = ObservableArrayList() + private val installSectionList = ObservableArrayList() + private val updatableSectionList = ObservableArrayList() private val itemsInstalled = diffListOf() private val itemsUpdatable = diffListOf() private val itemsRemote = diffListOf() - val adapter = adapterOf>() - val items = MergeObservableList>() + 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() + val items = MergeObservableList() .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> { + val itemBinding = itemBindingOf { 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) = withContext(Dispatchers.IO) { - installed - .mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) } - .map { RepoItem.Update(it) } - } + override fun onChanged(it: Pair?) { + val (progress, subject) = it ?: return + if (subject !is DownloadSubject.Module) + return - private suspend fun List.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() diff --git a/app/src/main/res/layout/fragment_module_md2.xml b/app/src/main/res/layout/fragment_module_md2.xml index fd1cf753b..e3a460118 100644 --- a/app/src/main/res/layout/fragment_module_md2.xml +++ b/app/src/main/res/layout/fragment_module_md2.xml @@ -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}" diff --git a/app/src/main/res/layout/item_section_md2.xml b/app/src/main/res/layout/item_section_md2.xml index 65568bda5..1953d1fc8 100644 --- a/app/src/main/res/layout/item_section_md2.xml +++ b/app/src/main/res/layout/item_section_md2.xml @@ -15,42 +15,36 @@ - + android:gravity="center_vertical"> - + tools:text="Installed" /> + tools:text="Reboot" /> - + diff --git a/app/src/main/res/layout/item_settings_section.xml b/app/src/main/res/layout/item_settings_section.xml index 92e751ee6..be304a526 100644 --- a/app/src/main/res/layout/item_settings_section.xml +++ b/app/src/main/res/layout/item_settings_section.xml @@ -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 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values/styles_md2_appearance.xml b/app/src/main/res/values/styles_md2_appearance.xml index 89069efa8..97d7897ad 100644 --- a/app/src/main/res/values/styles_md2_appearance.xml +++ b/app/src/main/res/values/styles_md2_appearance.xml @@ -2,19 +2,19 @@ - - - - @@ -119,4 +119,4 @@ - \ No newline at end of file +