Remove online section in modules fragment

This commit is contained in:
topjohnwu 2021-08-31 02:46:29 -07:00
parent f98c68a280
commit f5c982355a
7 changed files with 53 additions and 254 deletions

View File

@ -232,7 +232,7 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.preference:preference:1.1.1")

View File

@ -19,7 +19,6 @@ import com.topjohnwu.magisk.ktx.deviceProtectedContext
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
@ -61,7 +60,6 @@ object ServiceLocator {
return when (clz) {
HomeViewModel::class.java -> HomeViewModel(networkService)
LogViewModel::class.java -> LogViewModel(logRepo)
ModuleViewModel::class.java -> ModuleViewModel(repoDB, repoUpdater)
SettingsViewModel::class.java -> SettingsViewModel(repoDB)
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
InstallViewModel::class.java -> InstallViewModel(networkService)

View File

@ -10,8 +10,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.arch.ReselectionTarget
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.ktx.*
@ -42,13 +40,10 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
super.onStart()
setHasOptionsMenu(true)
activity.title = resources.getString(R.string.modules)
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setEndlessScroller()
setEndlessSearch()
binding.moduleFilterToggle.setOnClickListener {
isFilterVisible = true
@ -116,13 +111,6 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
// ---
override fun onEventDispatched(event: ViewEvent) = when (event) {
is EndlessRecyclerScrollListener.ResetState -> listeners.forEach { it.resetState() }
else -> super.onEventDispatched(event)
}
// ---
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_module_md2, menu)
}
@ -137,32 +125,12 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
// ---
override fun onReselected() {
binding.moduleList
.also { it.scrollToPosition(10) }
.let { binding.moduleList }
.also { it.post { it.smoothScrollToPosition(0) } }
binding.moduleList.scrollToPosition(10)
binding.moduleList.also { it.post { it.smoothScrollToPosition(0) } }
}
// ---
override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit
private fun setEndlessScroller() {
val lama = binding.moduleList.layoutManager ?: return
lama.isAutoMeasureEnabled = false
val listener = EndlessRecyclerScrollListener(lama, viewModel::loadRemote)
binding.moduleList.addOnScrollListener(listener)
listeners.add(listener)
}
private fun setEndlessSearch() {
val lama = binding.moduleFilterInclude.moduleFilterList.layoutManager ?: return
lama.isAutoMeasureEnabled = false
val listener = EndlessRecyclerScrollListener(lama, viewModel::loadMoreQuery)
binding.moduleFilterInclude.moduleFilterList.addOnScrollListener(listener)
listeners.add(listener)
}
}

View File

@ -39,34 +39,27 @@ class SectionTitle(
override fun contentSameAs(other: SectionTitle): Boolean = this === other
}
sealed class RepoItem(val item: OnlineModule) : ObservableItem<RepoItem>() {
class OnlineModuleRvItem(val item: OnlineModule) : ObservableItem<OnlineModuleRvItem>() {
override val layoutRes: Int = R.layout.item_repo_md2
@get:Bindable
var progress = 0
set(value) = set(value, field, { field = it }, BR.progress)
abstract val isUpdate: Boolean
var hasUpdate = false
override fun contentSameAs(other: RepoItem): Boolean = item == other.item
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
override fun contentSameAs(other: OnlineModuleRvItem): Boolean = item == other.item
override fun itemSameAs(other: OnlineModuleRvItem): Boolean = item.id == other.item.id
class Update(item: OnlineModule) : RepoItem(item) {
override val isUpdate get() = true
}
class Remote(item: OnlineModule) : RepoItem(item) {
override val isUpdate get() = false
}
}
class ModuleItem(val item: LocalModule) : ObservableItem<ModuleItem>() {
class LocalModuleRvItem(val item: LocalModule) : ObservableItem<LocalModuleRvItem>() {
override val layoutRes = R.layout.item_module_md2
@get:Bindable
var repo: OnlineModule? = null
set(value) = set(value, field, { field = it }, BR.repo)
var online: OnlineModule? = null
set(value) = set(value, field, { field = it }, BR.online)
@get:Bindable
var isEnabled = item.enable
@ -88,11 +81,10 @@ class ModuleItem(val item: LocalModule) : ObservableItem<ModuleItem>() {
viewModel.updateActiveState()
}
override fun contentSameAs(other: ModuleItem): Boolean = item.version == other.item.version
override fun contentSameAs(other: LocalModuleRvItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.name == other.item.name
override fun itemSameAs(other: ModuleItem): Boolean = item.id == other.item.id
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id
}

View File

@ -6,12 +6,10 @@ import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.events.OpenReadmeEvent
import com.topjohnwu.magisk.events.SelectModuleEvent
@ -19,37 +17,16 @@ import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ktx.addOnListChangedCallback
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.set
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
/*
* The repo fetching behavior should follow these rules:
*
* For the first time the repo list is queried in the app, it should ALWAYS fetch for
* updates. However, this particular fetch should go through RepoUpdater.invoke(false),
* which internally will set ETAGs when doing GET requests to GitHub's API and will
* only update repo DB only if the GitHub API shows that something is changed remotely.
*
* When a user explicitly requests a full DB refresh, it should ALWAYS do a full force
* refresh, which in code can be done with RepoUpdater.invoke(true). This will update
* every single repo's information regardless whether GitHub's API shows if there is
* anything changed or not.
* */
class ModuleViewModel(
private val repoDB: RepoDao,
private val repoUpdater: RepoUpdater
) : BaseViewModel(), Queryable {
class ModuleViewModel : BaseViewModel(), Queryable {
override val queryDelay = 1000L
private var queryJob: Job? = null
private var remoteJob: Job? = null
@get:Bindable
var isRemoteLoading = false
@ -67,57 +44,27 @@ class ModuleViewModel(
var searchLoading = false
set(value) = set(value, field, { field = it }, BR.searchLoading)
val itemsSearch = diffListOf<RepoItem>()
val itemSearchBinding = itemBindingOf<RepoItem> {
val itemsSearch = diffListOf<ComparableRvItem<*>>()
val itemSearchBinding = itemBindingOf<ComparableRvItem<*>> {
it.bindExtra(BR.viewModel, this)
}
private val installSectionList = ObservableArrayList<RvItem>()
private val updatableSectionList = ObservableArrayList<RvItem>()
private val itemsInstalled = diffListOf<ModuleItem>()
private val itemsUpdatable = diffListOf<RepoItem.Update>()
private val itemsOnline = diffListOf<RepoItem.Remote>()
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 itemsInstalled = diffListOf<LocalModuleRvItem>()
private val sectionInstalled = SectionTitle(
R.string.module_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
private val sectionOnline = SectionTitle(
R.string.module_section_online,
R.string.sorting_order
).apply { updateOrderIcon() }
val adapter = adapterOf<RvItem>()
val items = MergeObservableList<RvItem>()
.also { if (Info.env.isActive) {
it.insertItem(InstallModule)
.insertList(updatableSectionList)
.insertList(itemsUpdatable)
.insertList(installSectionList)
.insertList(itemsInstalled)
} }
.insertItem(sectionOnline)
.insertList(itemsOnline)
val itemBinding = itemBindingOf<RvItem> {
it.bindExtra(BR.viewModel, this)
}
// ---
private var refetch = false
// ---
init {
itemsInstalled.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ ->
@ -129,131 +76,58 @@ class ModuleViewModel(
installSectionList.clear()
}
)
itemsUpdatable.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ ->
if (updatableSectionList.isEmpty())
updatableSectionList.add(sectionUpdate)
},
onItemRangeRemoved = { list, _, _ ->
if (list.isEmpty())
updatableSectionList.clear()
}
)
if (Info.env.isActive) {
items.insertItem(InstallModule)
.insertList(installSectionList)
.insertList(itemsInstalled)
}
}
// ---
fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject !is Subject.Module)
return
viewModelScope.launch {
val items = withContext(Dispatchers.Default) {
val predicate = { it: RepoItem -> it.item.id == subject.module.id }
itemsUpdatable.filter(predicate) +
itemsOnline.filter(predicate) +
itemsSearch.filter(predicate)
}
items.forEach { it.progress = progress.times(100).roundToInt() }
}
}
override fun refresh(): Job {
return viewModelScope.launch {
state = State.LOADING
loadInstalled()
if (itemsOnline.isEmpty())
loadRemote()
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 = LocalModule.installed().map { ModuleItem(it) }
val installed = LocalModule.installed().map { LocalModuleRvItem(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 = repoDB.getModule(it.item.id)
}
}
val updates = itemsInstalled
.mapNotNull { repoDB.getUpdatableModule(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() {
// check for existing jobs
if (remoteJob?.isActive == true)
return
if (itemsOnline.isEmpty())
EndlessRecyclerScrollListener.ResetState().publish()
remoteJob = viewModelScope.launch {
suspend fun loadRemoteDB(offset: Int) = withContext(Dispatchers.IO) {
repoDB.getModules(offset).map { RepoItem.Remote(it) }
}
isRemoteLoading = true
val repos = if (itemsOnline.isEmpty()) {
repoUpdater.run(refetch)
loadUpdatable()
loadRemoteDB(0)
} else {
loadRemoteDB(itemsOnline.size)
}
isRemoteLoading = false
refetch = false
queryHandler.post { itemsOnline.addAll(repos) }
}
}
fun forceRefresh() {
itemsOnline.clear()
itemsUpdatable.clear()
itemsSearch.clear()
refetch = true
itemsInstalled.clear()
refresh()
submitQuery()
}
// ---
private suspend fun queryInternal(query: String, offset: Int): List<RepoItem> {
private suspend fun queryInternal(query: String): List<ComparableRvItem<*>> {
return if (query.isBlank()) {
itemsSearch.clear()
listOf()
} else {
withContext(Dispatchers.IO) {
repoDB.searchModules(query, offset).map { RepoItem.Remote(it) }
withContext(Dispatchers.Default) {
itemsInstalled.filter {
it.item.id.contains(query, true)
|| it.item.name.contains(query, true)
|| it.item.description.contains(query, true)
}
}
}
}
override fun query() {
EndlessRecyclerScrollListener.ResetState().publish()
queryJob = viewModelScope.launch {
val searched = queryInternal(query, 0)
viewModelScope.launch {
val searched = queryInternal(query)
val diff = withContext(Dispatchers.Default) {
itemsSearch.calculateDiff(searched)
}
@ -262,59 +136,28 @@ class ModuleViewModel(
}
}
fun loadMoreQuery() {
if (queryJob?.isActive == true) return
queryJob = viewModelScope.launch {
val searched = queryInternal(query, itemsSearch.size)
queryHandler.post { itemsSearch.addAll(searched) }
}
}
// ---
fun updateActiveState() = viewModelScope.launch {
sectionInstalled.hasButton = withContext(Dispatchers.Default) {
itemsInstalled.any { it.isModified }
}
fun updateActiveState() {
sectionInstalled.hasButton = itemsInstalled.any { it.isModified }
}
fun sectionPressed(item: SectionTitle) = when (item) {
sectionInstalled -> reboot() // TODO add reboot picker, regular reboot is not always preferred
sectionOnline -> {
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
}
sectionOnline.updateOrderIcon()
queryHandler.post {
itemsOnline.clear()
loadRemote()
}
Unit
}
sectionInstalled -> reboot()
else -> Unit
}
fun downloadPressed(item: RepoItem) = if (isConnected.get()) withExternalRW {
ModuleInstallDialog(item.item).publish()
} else {
SnackbarEvent(R.string.no_connection).publish()
}
// The following methods are not used, but kept for future integration
fun installPressed() = withExternalRW {
SelectModuleEvent().publish()
}
fun downloadPressed(item: OnlineModule) =
if (isConnected.get()) withExternalRW { ModuleInstallDialog(item).publish() }
else { SnackbarEvent(R.string.no_connection).publish() }
fun infoPressed(item: RepoItem) =
if (isConnected.get()) OpenReadmeEvent(item.item).publish()
fun installPressed() = withExternalRW { SelectModuleEvent().publish() }
fun infoPressed(item: OnlineModule) =
if (isConnected.get()) OpenReadmeEvent(item).publish()
else SnackbarEvent(R.string.no_connection).publish()
fun infoPressed(item: ModuleItem) {
item.repo?.also {
if (isConnected.get()) OpenReadmeEvent(it).publish()
else SnackbarEvent(R.string.no_connection).publish()
} ?: return
}
fun infoPressed(item: LocalModuleRvItem) { infoPressed(item.online ?: return) }
}

View File

@ -11,7 +11,7 @@
<variable
name="item"
type="com.topjohnwu.magisk.ui.module.ModuleItem" />
type="com.topjohnwu.magisk.ui.module.LocalModuleRvItem" />
<variable
name="viewModel"
@ -120,7 +120,7 @@
<ImageView
android:id="@+id/module_info"
style="@style/WidgetFoundation.Icon"
gone="@{item.repo == null}"
gone="@{item.online == null}"
android:layout_width="wrap_content"
android:alpha=".5"
android:clickable="true"

View File

@ -9,7 +9,7 @@
<variable
name="item"
type="com.topjohnwu.magisk.ui.module.RepoItem" />
type="com.topjohnwu.magisk.ui.module.OnlineModuleRvItem" />
<variable
name="viewModel"
@ -25,7 +25,6 @@
android:clickable="true"
android:focusable="true"
android:nextFocusRight="@id/module_info"
android:onClick="@{() -> viewModel.downloadPressed(item)}"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1">
@ -102,7 +101,7 @@
android:layout_width="wrap_content"
android:alpha=".5"
android:nextFocusLeft="@id/module_card"
android:onClick="@{() -> viewModel.infoPressed(item)}"
android:onClick="@{() -> viewModel.infoPressed(item.item)}"
android:paddingEnd="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="@+id/module_download"
app:layout_constraintEnd_toStartOf="@+id/module_download"
@ -113,11 +112,11 @@
android:id="@+id/module_download"
style="@style/WidgetFoundation.Icon.Primary"
isEnabled="@{!(item.progress == -100 || (item.progress > 0 &amp;&amp; item.progress &lt; 100))}"
srcCompat="@{item.isUpdate ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}"
srcCompat="@{item.hasUpdate ? R.drawable.ic_update_md2 : R.drawable.ic_download_md2}"
android:layout_width="wrap_content"
android:contentDescription="@string/download"
android:nextFocusLeft="@id/module_info"
android:onClick="@{() -> viewModel.downloadPressed(item)}"
android:onClick="@{() -> viewModel.downloadPressed(item.item)}"
android:paddingStart="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -154,4 +153,3 @@
</com.google.android.material.card.MaterialCardView>
</layout>