Support modules update

This commit is contained in:
vvb2060 2022-01-17 15:08:54 +08:00 committed by John Wu
parent 2997258fd0
commit bc0c1980db
15 changed files with 120 additions and 664 deletions

View File

@ -43,7 +43,7 @@ sealed class Subject : Parcelable {
val action: Action, val action: Action,
override val notifyId: Int = Notifications.nextId() override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val url: String get() = module.zip_url override val url: String get() = module.zipUrl
override val title: String get() = module.downloadFilename override val title: String get() = module.downloadFilename
@IgnoredOnParcel @IgnoredOnParcel

View File

@ -28,11 +28,9 @@ data class StubJson(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ModuleJson( data class ModuleJson(
val id: String, val version: String,
val last_update: Long, val versionCode: Int,
val prop_url: String, val zipUrl: String,
val zip_url: String,
val notes_url: String
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View File

@ -1,21 +1,26 @@
package com.topjohnwu.magisk.core.model.module package com.topjohnwu.magisk.core.model.module
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.util.* import java.util.*
data class LocalModule( data class LocalModule(
private val path: String, private val path: String,
override var id: String = "",
override var name: String = "",
override var author: String = "",
override var version: String = "",
override var versionCode: Int = -1,
override var description: String = "",
) : Module() { ) : Module() {
override var id: String = ""
override var name: String = ""
override var version: String = ""
override var versionCode: Int = -1
var author: String = ""
var description: String = ""
var updateJson: String = ""
var updateInfo: OnlineModule? = null
private val removeFile = SuFile(path, "remove") private val removeFile = SuFile(path, "remove")
private val disableFile = SuFile(path, "disable") private val disableFile = SuFile(path, "disable")
@ -66,6 +71,30 @@ data class LocalModule(
} }
} }
@Throws(NumberFormatException::class)
private fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
"updateJson" -> updateJson = value
}
}
}
init { init {
runCatching { runCatching {
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out) parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
@ -81,13 +110,28 @@ data class LocalModule(
} }
} }
suspend fun load():Boolean {
if (updateJson.isEmpty()) return false
try {
val json = ServiceLocator.networkService.fetchModuleJson(updateJson)
if (json.versionCode > versionCode) {
updateInfo = OnlineModule(this, json)
return true
}
} catch (e: IOException) {
Timber.w(e)
}
return false
}
companion object { companion object {
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk" private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
suspend fun installed() = withContext(Dispatchers.IO) { suspend fun installed() = withContext(Dispatchers.IO) {
SuFile(Const.MAGISK_PATH) SuFile(Const.MAGISK_PATH)
.listFiles { _, name -> name != "lost+found" && name != ".core" } .listFiles()
.orEmpty() .orEmpty()
.filter { !it.isFile } .filter { !it.isFile }
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") } .map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }

View File

@ -5,37 +5,10 @@ abstract class Module : Comparable<Module> {
protected set protected set
abstract var name: String abstract var name: String
protected set protected set
abstract var author: String
protected set
abstract var version: String abstract var version: String
protected set protected set
abstract var versionCode: Int abstract var versionCode: Int
protected set protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class) override operator fun compareTo(other: Module) = id.compareTo(other.id)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
}
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
} }

View File

@ -1,65 +1,26 @@
package com.topjohnwu.magisk.core.model.module package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.model.ModuleJson import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.legalFilename
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.text.DateFormat
import java.util.*
@Entity(tableName = "modules")
@Parcelize @Parcelize
data class OnlineModule( data class OnlineModule(
@PrimaryKey override var id: String, override var id: String,
override var name: String = "", override var name: String,
override var author: String = "", override var version: String,
override var version: String = "", override var versionCode: Int,
override var versionCode: Int = -1, val zipUrl: String,
override var description: String = "",
val last_update: Long,
val prop_url: String,
val zip_url: String,
val notes_url: String
) : Module(), Parcelable { ) : Module(), Parcelable {
constructor(local: LocalModule, json: ModuleJson) :
this(local.id, local.name, json.version, json.versionCode, json.zipUrl)
private val svc get() = ServiceLocator.networkService
constructor(info: ModuleJson) : this(
id = info.id,
last_update = info.last_update,
prop_url = info.prop_url,
zip_url = info.zip_url,
notes_url = info.notes_url
)
val lastUpdate get() = Date(last_update)
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename() val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
suspend fun notes() = svc.fetchString(notes_url) private fun String.legalFilename() = replace(" ", "_")
.replace("'", "").replace("\"", "")
@Throws(IllegalRepoException::class) .replace("$", "").replace("`", "")
suspend fun load() { .replace("*", "").replace("/", "_")
try { .replace("#", "").replace("@", "")
val rawProps = svc.fetchString(prop_url) .replace("\\", "_")
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
parseProps(props)
} catch (e: Exception) {
throw IllegalRepoException("Repo [$id] parse error:", e)
}
if (versionCode < 0) {
throw IllegalRepoException("Repo [$id] does not contain versionCode")
}
}
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
companion object {
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
}
} }

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.data.network package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.core.model.BranchInfo import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.http.* import retrofit2.http.*
@ -37,6 +38,9 @@ interface RawServices {
@GET @GET
suspend fun fetchString(@Url url: String): String suspend fun fetchString(@Url url: String): String
@GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson
} }
interface GithubApiServices { interface GithubApiServices {

View File

@ -65,6 +65,7 @@ class NetworkService(
} }
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) } suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) } suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
} }

View File

@ -5,7 +5,6 @@ import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.DiffRvItem import com.topjohnwu.magisk.databinding.DiffRvItem
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
import com.topjohnwu.magisk.databinding.RvContainer import com.topjohnwu.magisk.databinding.RvContainer
@ -16,62 +15,29 @@ object InstallModule : DiffRvItem<InstallModule>() {
override val layoutRes = R.layout.item_module_download override val layoutRes = R.layout.item_module_download
} }
class SectionTitle(
val title: Int,
_button: Int = 0,
_icon: Int = 0
) : ObservableDiffRvItem<SectionTitle>() {
override val layoutRes = R.layout.item_section_md2
@get:Bindable
var button = _button
set(value) = set(value, field, { field = it }, BR.button)
@get:Bindable
var icon = _icon
set(value) = set(value, field, { field = it }, BR.icon)
@get:Bindable
var hasButton = _button != 0 && _icon != 0
set(value) = set(value, field, { field = it }, BR.hasButton)
}
class OnlineModuleRvItem(
override val item: OnlineModule
) : ObservableDiffRvItem<OnlineModuleRvItem>(), RvContainer<OnlineModule> {
override val layoutRes: Int = R.layout.item_repo_md2
@get:Bindable
var progress = 0
set(value) = set(value, field, { field = it }, BR.progress)
var hasUpdate = false
override fun itemSameAs(other: OnlineModuleRvItem): Boolean = item.id == other.item.id
}
class LocalModuleRvItem( class LocalModuleRvItem(
override val item: LocalModule override val item: LocalModule
) : ObservableDiffRvItem<LocalModuleRvItem>(), RvContainer<LocalModule> { ) : ObservableDiffRvItem<LocalModuleRvItem>(), RvContainer<LocalModule> {
override val layoutRes = R.layout.item_module_md2 override val layoutRes = R.layout.item_module_md2
@get:Bindable
var online: OnlineModule? = null
set(value) = set(value, field, { field = it }, BR.online)
@get:Bindable @get:Bindable
var isEnabled = item.enable var isEnabled = item.enable
set(value) = set(value, field, { field = it }, BR.enabled) { set(value) = set(value, field, { field = it }, BR.enabled, BR.updateReady) {
item.enable = value item.enable = value
} }
@get:Bindable @get:Bindable
var isRemoved = item.remove var isRemoved = item.remove
set(value) = set(value, field, { field = it }, BR.removed) { set(value) = set(value, field, { field = it }, BR.removed, BR.updateReady) {
item.remove = value item.remove = value
} }
@get:Bindable
var updateReady: Boolean
get() = item.updateInfo != null && !isRemoved && isEnabled
set(_) = notifyPropertyChanged(BR.updateReady)
val isSuspended = val isSuspended =
(Info.isZygiskEnabled && item.isRiru) || (!Info.isZygiskEnabled && item.isZygisk) (Info.isZygiskEnabled && item.isRiru) || (!Info.isZygiskEnabled && item.isZygisk)
@ -80,11 +46,9 @@ class LocalModuleRvItem(
else R.string.suspend_text_zygisk.asText(R.string.zygisk.asText()) else R.string.suspend_text_zygisk.asText(R.string.zygisk.asText())
val isUpdated get() = item.updated val isUpdated get() = item.updated
val isModified get() = isRemoved || isUpdated
fun delete(viewModel: ModuleViewModel) { fun delete() {
isRemoved = !isRemoved isRemoved = !isRemoved
viewModel.updateActiveState()
} }
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id

View File

@ -1,63 +1,30 @@
package com.topjohnwu.magisk.ui.module package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.Queryable
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.* import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.events.OpenReadmeEvent import com.topjohnwu.magisk.databinding.adapterOf
import com.topjohnwu.magisk.databinding.diffListOf
import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.events.SelectModuleEvent import com.topjohnwu.magisk.events.SelectModuleEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ktx.addOnListChangedCallback
import com.topjohnwu.magisk.ktx.reboot
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class ModuleViewModel : BaseViewModel(), Queryable { class ModuleViewModel : BaseViewModel() {
val bottomBarBarrierIds = val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
intArrayOf(R.id.module_info, R.id.module_remove)
override val queryDelay = 1000L
@get:Bindable
var isRemoteLoading = false
set(value) = set(value, field, { field = it }, BR.remoteLoading)
@get:Bindable
var query = ""
set(value) = set(value, field, { field = it }, BR.query) {
submitQuery()
// Yes we do lie about the search being loaded
searchLoading = true
}
@get:Bindable
var searchLoading = false
set(value) = set(value, field, { field = it }, BR.searchLoading)
val itemsSearch = diffListOf<AnyDiffRvItem>()
val itemSearchBinding = itemBindingOf<AnyDiffRvItem> {
it.bindExtra(BR.viewModel, this)
}
private val installSectionList = ObservableArrayList<RvItem>()
private val itemsInstalled = diffListOf<LocalModuleRvItem>() private val itemsInstalled = diffListOf<LocalModuleRvItem>()
private val sectionInstalled = SectionTitle(
R.string.module_installed,
R.string.reboot,
R.drawable.ic_restart
).also { it.hasButton = false }
val adapter = adapterOf<RvItem>() val adapter = adapterOf<RvItem>()
val items = MergeObservableList<RvItem>() val items = MergeObservableList<RvItem>()
@ -65,34 +32,19 @@ class ModuleViewModel : BaseViewModel(), Queryable {
it.bindExtra(BR.viewModel, this) it.bindExtra(BR.viewModel, this)
} }
// ---
init { init {
itemsInstalled.addOnListChangedCallback(
onItemRangeInserted = { _, _, _ ->
if (installSectionList.isEmpty())
installSectionList.add(sectionInstalled)
},
onItemRangeRemoved = { list, _, _ ->
if (list.isEmpty())
installSectionList.clear()
}
)
if (Info.env.isActive) { if (Info.env.isActive) {
items.insertItem(InstallModule) items.insertItem(InstallModule)
.insertList(installSectionList)
.insertList(itemsInstalled) .insertList(itemsInstalled)
} }
} }
// ---
override fun refresh(): Job { override fun refresh(): Job {
return viewModelScope.launch { return viewModelScope.launch {
state = State.LOADING state = State.LOADING
loadInstalled() loadInstalled()
state = State.LOADED state = State.LOADED
loadUpdateInfo()
} }
} }
@ -104,62 +56,21 @@ class ModuleViewModel : BaseViewModel(), Queryable {
itemsInstalled.update(installed, diff) itemsInstalled.update(installed, diff)
} }
fun forceRefresh() { private suspend fun loadUpdateInfo() {
itemsInstalled.clear() withContext(Dispatchers.IO) {
refresh() itemsInstalled.forEach {
submitQuery() it.updateReady = it.item.load()
}
}
} }
// --- fun downloadPressed(item: OnlineModule?) =
if (item != null && isConnected.get()) {
private suspend fun queryInternal(query: String): List<AnyDiffRvItem> { withExternalRW { ModuleInstallDialog(item).publish() }
return if (query.isBlank()) {
itemsSearch.clear()
listOf()
} else { } else {
withContext(Dispatchers.Default) { SnackbarEvent(R.string.no_connection).publish()
itemsInstalled.filter {
it.item.id.contains(query, true)
|| it.item.name.contains(query, true)
|| it.item.description.contains(query, true)
} }
}
}
}
override fun query() {
viewModelScope.launch {
val searched = queryInternal(query)
val diff = withContext(Dispatchers.Default) {
itemsSearch.calculateDiff(searched)
}
searchLoading = false
itemsSearch.update(searched, diff)
}
}
// ---
fun updateActiveState() {
sectionInstalled.hasButton = itemsInstalled.any { it.isModified }
}
fun sectionPressed(item: SectionTitle) = when (item) {
sectionInstalled -> reboot()
else -> Unit
}
// The following methods are not used, but kept for future integration
fun downloadPressed(item: OnlineModule) =
if (isConnected.get()) withExternalRW { ModuleInstallDialog(item).publish() }
else { SnackbarEvent(R.string.no_connection).publish() }
fun installPressed() = withExternalRW { SelectModuleEvent().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: LocalModuleRvItem) { infoPressed(item.online ?: return) }
} }

View File

@ -17,21 +17,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<!--
The list is reverted to use LinearLayoutManager only. The issue of random crashes lies in
the way StaggeredGridLayoutManager invalidates view in LazySpanLookup. Since we're adding
items in between full-span items the array is not yet invalidated and consecutively crashes
due to index of -end- being out of bounds of the current array.
If you'd like to use StaggeredGridLayoutManager, do so without adding single span items in
between of full-span items.
https://issuetracker.google.com/issues/37034096
-->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_list" android:id="@+id/module_list"
adapter="@{viewModel.adapter}" adapter="@{viewModel.adapter}"
gone="@{viewModel.loading &amp;&amp; viewModel.items.empty}" gone="@{viewModel.loading}"
itemBinding="@{viewModel.itemBinding}" itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}" items="@{viewModel.items}"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -39,46 +28,13 @@
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/internal_action_bar_size" android:paddingTop="@dimen/internal_action_bar_size"
android:paddingBottom="56dp" android:paddingBottom="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom" app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_module_md2" /> tools:listitem="@layout/item_module_md2" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/module_filter_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="72dp"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:layout_fitsSystemWindowsInsets="bottom"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" />
<com.google.android.material.circularreveal.cardview.CircularRevealCardView
android:id="@+id/module_filter"
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:visibility="invisible"
app:cardBackgroundColor="?colorSurface"
app:cardCornerRadius="0dp">
<include
android:id="@+id/module_filter_include"
layout="@layout/include_module_filter"
viewModel="@{viewModel}"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.circularreveal.cardview.CircularRevealCardView>
<LinearLayout <LinearLayout
goneUnless="@{viewModel.loading &amp;&amp; viewModel.items.empty}" goneUnless="@{viewModel.loading}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -98,28 +54,6 @@
</LinearLayout> </LinearLayout>
<FrameLayout
goneUnless="@{viewModel.remoteLoading}"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_gravity="bottom">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_gravity="center"
tools:ignore="UselessParent">
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>
</FrameLayout> </FrameLayout>

View File

@ -1,135 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.core.Config" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingBottom="@dimen/l1"
app:fitsSystemWindowsInsets="bottom"
tools:layout_gravity="bottom"
tools:paddingBottom="64dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_filter_list"
itemBinding="@{viewModel.itemSearchBinding}"
items="@{viewModel.itemsSearch}"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/l1"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/module_filter_title_search"
app:layout_constraintTop_toTopOf="parent"
app:reverseLayout="false"
app:spanCount="2"
tools:listitem="@layout/item_repo_md2" />
<ImageView
android:layout_width="match_parent"
android:layout_height="@dimen/l_50"
app:srcCompat="@drawable/bg_shadow"
app:tint="?colorSurfaceVariant"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_list" />
<TextView
android:id="@+id/module_filter_title_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l1"
android:text="@string/hide_search"
android:textAllCaps="true"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textColor="?colorPrimary"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/module_filter_search" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/module_filter_search"
style="@style/WidgetFoundation.Card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
app:cardCornerRadius="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/module_filter_done"
app:layout_constraintStart_toStartOf="parent">
<ImageView
style="@style/WidgetFoundation.Icon"
android:layout_width="48dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical|start"
android:padding="6dp"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorDisabled" />
<EditText
android:id="@+id/module_filter_search_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:background="@null"
android:hint="@string/hide_filter_hint"
android:inputType="textUri"
android:minHeight="36dp"
android:nextFocusRight="@id/module_filter_done"
android:paddingStart="0dp"
android:paddingEnd="@dimen/l1"
android:singleLine="true"
android:text="@={viewModel.query}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="@color/color_text_transient"
android:textColorHint="?colorOnSurfaceVariant" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/module_filter_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/l1"
android:nextFocusLeft="@id/module_filter_search_field"
app:backgroundTint="?colorPrimary"
app:elevation="0dp"
app:fabSize="mini"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/module_filter_search"
app:srcCompat="@drawable/ic_check_md2"
app:tint="?colorOnPrimary" />
<ProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate.Circular"
goneUnless="@{viewModel.searchLoading}"
android:layout_width="56dp"
android:layout_height="56dp"
app:layout_constraintBottom_toBottomOf="@+id/module_filter_done"
app:layout_constraintEnd_toEndOf="@+id/module_filter_done"
app:layout_constraintStart_toStartOf="@+id/module_filter_done"
app:layout_constraintTop_toTopOf="@+id/module_filter_done" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -18,6 +18,7 @@
style="@style/WidgetFoundation.Button.Outlined" style="@style/WidgetFoundation.Button.Outlined"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/l_50"
android:onClick="@{() -> viewModel.installPressed()}" android:onClick="@{() -> viewModel.installPressed()}"
android:text="@string/module_action_install_external" android:text="@string/module_action_install_external"
android:textAllCaps="false" android:textAllCaps="false"

View File

@ -98,7 +98,7 @@
app:layout_constraintBottom_toBottomOf="@+id/module_version_author" app:layout_constraintBottom_toBottomOf="@+id/module_version_author"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1" app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@+id/module_info" app:layout_constraintStart_toEndOf="@+id/module_update"
app:layout_constraintTop_toTopOf="@+id/module_title" /> app:layout_constraintTop_toTopOf="@+id/module_title" />
<TextView <TextView
@ -111,6 +111,7 @@
android:layout_marginTop="@dimen/l1" android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1" android:layout_marginEnd="@dimen/l1"
android:duplicateParentState="true" android:duplicateParentState="true"
android:maxLines="5"
android:text="@{item.item.description}" android:text="@{item.item.description}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
@ -126,30 +127,34 @@
android:background="?colorSurfaceSurfaceVariant" android:background="?colorSurfaceSurfaceVariant"
app:layout_constraintTop_toBottomOf="@+id/module_description" /> app:layout_constraintTop_toBottomOf="@+id/module_description" />
<ImageView <Button
android:id="@+id/module_info" android:id="@+id/module_update"
style="@style/WidgetFoundation.Icon" style="@style/WidgetFoundation.Button.Text"
gone="@{item.online == null}" gone="@{item.item.updateJson.length == 0}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:alpha=".5" android:layout_height="wrap_content"
android:clickable="true" android:clickable="true"
android:enabled="@{item.updateReady}"
android:focusable="true" android:focusable="true"
android:onClick="@{() -> viewModel.infoPressed(item)}" android:onClick="@{() -> viewModel.downloadPressed(item.item.updateInfo)}"
android:paddingEnd="@dimen/l_50" android:text="@string/update"
android:textAllCaps="false"
app:icon="@drawable/ic_update_md2"
app:iconGravity="textEnd"
app:layout_constraintBottom_toBottomOf="@+id/module_remove" app:layout_constraintBottom_toBottomOf="@+id/module_remove"
app:layout_constraintEnd_toStartOf="@+id/module_remove" app:layout_constraintEnd_toStartOf="@+id/module_remove"
app:layout_constraintTop_toTopOf="@+id/module_remove" app:layout_constraintTop_toTopOf="@+id/module_remove"
app:srcCompat="@drawable/ic_info" /> app:srcCompat="@drawable/ic_download_md2" />
<Button <Button
android:id="@+id/module_remove" android:id="@+id/module_remove"
style="@style/WidgetFoundation.Button.Text" style="@style/WidgetFoundation.Button.Text.Secondary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="true" android:clickable="true"
android:enabled="@{!item.updated}" android:enabled="@{!item.updated}"
android:focusable="true" android:focusable="true"
android:onClick="@{() -> item.delete(viewModel)}" android:onClick="@{() -> item.delete()}"
android:text="@{item.removed ? @string/module_state_restore : @string/module_state_remove}" android:text="@{item.removed ? @string/module_state_restore : @string/module_state_remove}"
android:textAllCaps="false" android:textAllCaps="false"
app:icon="@{item.removed ? @drawable/ic_restart : @drawable/ic_delete_md2}" app:icon="@{item.removed ? @drawable/ic_restart : @drawable/ic_delete_md2}"
@ -165,7 +170,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="start" app:barrierDirection="start"
app:referencedIds="@{viewModel.bottomBarBarrierIds}" app:referencedIds="@{viewModel.bottomBarBarrierIds}"
tools:constraint_referenced_ids="module_info,module_remove" /> tools:constraint_referenced_ids="module_update,module_remove" />
<TextView <TextView
android:id="@+id/module_suspend_text" android:id="@+id/module_suspend_text"

View File

@ -1,155 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.topjohnwu.magisk.R" />
<variable
name="item"
type="com.topjohnwu.magisk.ui.module.OnlineModuleRvItem" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
android:id="@+id/module_card"
style="@style/WidgetFoundation.Card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:nextFocusRight="@id/module_info"
tools:layout_gravity="center"
tools:layout_marginBottom="@dimen/l1"
tools:layout_marginEnd="@dimen/l1">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/module_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.name}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/module_version_author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{@string/module_version_author(item.item.version ?? `?`, item.item.author ?? `?`)}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintEnd_toEndOf="@+id/module_title"
app:layout_constraintStart_toStartOf="@+id/module_title"
app:layout_constraintTop_toBottomOf="@+id/module_title"
tools:text="v1 by topjohnwu" />
<TextView
android:id="@+id/module_description"
gone="@{item.item.description.empty}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:layout_marginTop="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:text="@{item.item.description}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
app:layout_constraintTop_toBottomOf="@+id/module_version_author"
tools:lines="4"
tools:text="@tools:sample/lorem/random" />
<View
android:id="@+id/module_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/l1"
android:background="?colorSurfaceSurfaceVariant"
app:layout_constraintTop_toBottomOf="@+id/module_description" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/l1"
android:text="@{item.item.lastUpdateString}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textSize="11sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/module_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider"
tools:ignore="SmallSp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageView
android:id="@+id/module_info"
style="@style/WidgetFoundation.Icon"
android:layout_width="wrap_content"
android:alpha=".5"
android:nextFocusLeft="@id/module_card"
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"
app:layout_constraintTop_toTopOf="@+id/module_download"
app:srcCompat="@drawable/ic_info" />
<ImageView
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.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.item)}"
android:paddingStart="@dimen/l_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/module_divider"
tools:srcCompat="@drawable/ic_download_md2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<ProgressBar
style="@style/WidgetFoundation.ProgressBar"
goneUnless="@{item.progress > 0 &amp;&amp; item.progress &lt; 100}"
progressAnimated="@{item.progress}"
android:layout_width="match_parent"
android:layout_gravity="bottom"
tools:progress="40" />
<androidx.core.widget.ContentLoadingProgressBar
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
goneUnless="@{item.progress == -100}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="-4dp"
android:layout_marginBottom="-5dp"
tools:progress="40" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
</layout>

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.ui.module.SectionTitle" />
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.module.ModuleViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:gravity="center_vertical">
<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.Large"
android:textColor="@color/color_primary_transient"
android:textStyle="bold"
tools:text="Installed" />
<Button
android:id="@+id/module_button"
style="@style/WidgetFoundation.Button.Text.Secondary"
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}"
android:textAllCaps="false"
app:icon="@{item.icon}"
tools:text="Reboot" />
</LinearLayout>
</layout>