Migrate MagiskDialog to Compose and remove kapt/data binding

Replace MagiskDialog from AppCompatDialog with data binding to a
Compose state holder rendered via miuix SuperDialog. Update
MarkDownDialog to use AndroidView-based Compose content. Remove kapt
plugin, data binding, ObservableHost, RvItemAdapter, DataBindingAdapters,
and all remaining XML layouts (dialog_magisk_base, item_list_single_line,
markdown_window_md2). Clean up dead version catalog entries and remove
UIActivity generic type parameter.

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 14:13:13 +08:00
committed by topjohnwu
parent d7301154a5
commit 44a31234d3
19 changed files with 265 additions and 1021 deletions
-16
View File
@@ -3,23 +3,12 @@ plugins {
kotlin("plugin.parcelize")
kotlin("plugin.compose")
kotlin("plugin.serialization")
alias(libs.plugins.legacy.kapt)
}
setupMainApk()
kapt {
correctErrorTypes = true
useBuildCache = true
mapDiagnosticLocations = true
javacOptions {
option("-Xmaxerrs", "1000")
}
}
android {
buildFeatures {
dataBinding = true
compose = true
}
@@ -46,8 +35,6 @@ dependencies {
implementation(libs.rikka.layoutinflater)
implementation(libs.rikka.insets)
implementation(libs.constraintlayout)
implementation(libs.recyclerview)
implementation(libs.appcompat)
implementation(libs.material)
@@ -67,7 +54,4 @@ dependencies {
implementation(libs.navigation3.runtime)
implementation(libs.navigationevent.compose)
implementation(libs.lifecycle.viewmodel.navigation3)
// Make sure kapt runs with a proper kotlin-stdlib
kapt(kotlin("stdlib"))
}
@@ -5,12 +5,10 @@ import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.events.DialogEvent
@@ -20,9 +18,7 @@ import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
abstract class BaseViewModel : ViewModel(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
abstract class BaseViewModel : ViewModel() {
private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
@@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.ViewDataBinding
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
@@ -22,16 +21,12 @@ import com.topjohnwu.magisk.core.wrap
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class UIActivity<Binding : ViewDataBinding>
abstract class UIActivity
: AppCompatActivity(), ViewModelHolder, IActivityExtension {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
override val extension = ActivityExtension(this)
protected val binded get() = ::binding.isInitialized
open val snackbarView get() = binding.root
abstract val snackbarView: View
open val snackbarAnchorView: View? get() = null
init {
@@ -48,7 +43,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
extension.onCreate(savedInstanceState)
if (isRunningAsStub) {
// Overwrite private members to avoid nasty "false" stack traces being logged
val delegate = delegate
val clz = delegate.javaClass
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
@@ -59,8 +53,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
startObserveLiveData()
// We need to set the window background explicitly since for whatever reason it's not
// propagated upstream
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
.use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) }
@@ -69,7 +61,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
@@ -13,6 +13,6 @@ interface ContextExecutor {
}
interface ActivityExecutor {
operator fun invoke(activity: UIActivity<*>)
operator fun invoke(activity: UIActivity)
}
@@ -1,274 +0,0 @@
package com.topjohnwu.magisk.databinding
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.updateLayoutParams
import androidx.core.widget.ImageViewCompat
import androidx.databinding.BindingAdapter
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlin.math.roundToInt
@BindingAdapter("gone")
fun setGone(view: View, gone: Boolean) {
view.isGone = gone
}
@BindingAdapter("invisible")
fun setInvisible(view: View, invisible: Boolean) {
view.isInvisible = invisible
}
@BindingAdapter("goneUnless")
fun setGoneUnless(view: View, goneUnless: Boolean) {
setGone(view, goneUnless.not())
}
@BindingAdapter("invisibleUnless")
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
setInvisible(view, invisibleUnless.not())
}
@BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, markdown: Spanned) {
ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
}
@BindingAdapter("onNavigationClick")
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
view.setNavigationOnClickListener(listener)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
view.setImageResource(resId)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, drawable: Drawable) {
view.setImageDrawable(drawable)
}
@BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
view.setOnTouchListener(listener)
}
@BindingAdapter("scrollToLast")
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
fun scrollToLast() = UiThreadHandler.handler.postDelayed({
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
}, 30)
fun wait(callback: () -> Unit) {
UiThreadHandler.handler.postDelayed(callback, 1000)
}
fun RecyclerView.Adapter<*>.setListener() {
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
scrollToLast()
}
}
registerAdapterDataObserver(observer)
view.setTag(R.id.recyclerScrollListener, observer)
}
fun RecyclerView.Adapter<*>.removeListener() {
val observer =
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
unregisterAdapterDataObserver(observer)
}
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
if (shouldScrollToLast) {
trySetListener()
} else {
view.adapter?.removeListener()
}
}
@BindingAdapter("isEnabled")
fun setEnabled(view: View, isEnabled: Boolean) {
view.isEnabled = isEnabled
}
@BindingAdapter("error")
fun TextInputLayout.setErrorString(error: String) {
val newError = error.let { if (it.isEmpty()) null else it }
if (this.error == null && newError == null) return
this.error = newError
}
// md2
@BindingAdapter(
"android:layout_marginLeft",
"android:layout_marginTop",
"android:layout_marginRight",
"android:layout_marginBottom",
"android:layout_marginStart",
"android:layout_marginEnd",
requireAll = false
)
fun View.setMargins(
marginLeft: Int?,
marginTop: Int?,
marginRight: Int?,
marginBottom: Int?,
marginStart: Int?,
marginEnd: Int?
) = updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginLeft?.let { leftMargin = it }
marginTop?.let { topMargin = it }
marginRight?.let { rightMargin = it }
marginBottom?.let { bottomMargin = it }
marginStart?.let { this.marginStart = it }
marginEnd?.let { this.marginEnd = it }
}
@BindingAdapter("nestedScrollingEnabled")
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
isNestedScrollingEnabled = enabled
}
@BindingAdapter("isSelected")
fun View.isSelected(isSelected: Boolean) {
this.isSelected = isSelected
}
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
if (dividerHorizontal != null) {
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
setDrawable(dividerHorizontal)
}.let { addItemDecoration(it) }
}
if (dividerVertical != null) {
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
setDrawable(dividerVertical)
}.let { addItemDecoration(it) }
}
}
@BindingAdapter("icon")
fun Button.setIconRes(res: Int) {
(this as MaterialButton).setIconResource(res)
}
@BindingAdapter("icon")
fun Button.setIcon(drawable: Drawable) {
(this as MaterialButton).icon = drawable
}
@BindingAdapter("strokeWidth")
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
strokeWidth = stroke.roundToInt()
}
@BindingAdapter("onMenuClick")
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
setOnMenuItemClickListener(listener)
}
@BindingAdapter("progressAnimated")
fun ProgressBar.setProgressAnimated(newProgress: Int) {
val animator = tag as? ValueAnimator
animator?.cancel()
ValueAnimator.ofInt(progress, newProgress).apply {
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progress = it.animatedValue as Int }
tag = this
}.start()
}
@BindingAdapter("android:text")
fun TextView.setTextSafe(text: Int) {
if (text == 0) this.text = null else setText(text)
}
@BindingAdapter("android:onLongClick")
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
setOnLongClickListener {
listener()
true
}
}
@BindingAdapter("strikeThrough")
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
paintFlags = if (useStrikeThrough) {
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
@BindingAdapter("spanCount")
fun RecyclerView.setSpanCount(count: Int) {
when (val lama = layoutManager) {
is GridLayoutManager -> lama.spanCount = count
is StaggeredGridLayoutManager -> lama.spanCount = count
}
}
@BindingAdapter("cardBackgroundColorAttr")
fun CardView.setCardBackgroundColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setCardBackgroundColor(tv.data)
}
@BindingAdapter("tint")
fun ImageView.setTint(color: Int) {
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color))
}
@BindingAdapter("tintAttr")
fun ImageView.setTintAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(tv.data))
}
@BindingAdapter("textColorAttr")
fun TextView.setTextColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setTextColor(tv.data)
}
@BindingAdapter("android:text")
fun TextView.setText(text: TextHolder) {
this.text = text.getText(context.resources)
}
@@ -1,97 +0,0 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
/**
* Modified from https://github.com/skoumalcz/teanity/blob/1.2/core/src/main/java/com/skoumal/teanity/observable/Notifyable.kt
*
* Interface that allows user to be observed via DataBinding or manually by assigning listeners.
*
* @see [androidx.databinding.Observable]
* */
interface ObservableHost : Observable {
var callbacks: PropertyChangeRegistry?
/**
* Notifies all observers that something has changed. By default implementation this method is
* synchronous, hence observers will never be notified in undefined order. Observers might
* choose to refresh the view completely, which is beyond the scope of this function.
* */
fun notifyChange() {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, 0, null)
}
/**
* Notifies all observers about field with [fieldId] has been changed. This will happen
* synchronously before or after [notifyChange] has been called. It will never be called during
* the execution of aforementioned method.
* */
fun notifyPropertyChanged(fieldId: Int) {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, fieldId, null)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: PropertyChangeRegistry().also { callbacks = it }
}.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: return
}.remove(callback)
}
}
fun ObservableHost.addOnPropertyChangedCallback(
fieldId: Int,
removeAfterChanged: Boolean = false,
callback: () -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
if (fieldId == propertyId) {
callback()
if (removeAfterChanged)
removeOnPropertyChangedCallback(this)
}
}
})
}
/**
* Injects boilerplate implementation for {@literal @}[androidx.databinding.Bindable] field setters.
*
* # Examples:
* ```kotlin
* @get:Bindable
* var myField = defaultValue
* set(value) = set(value, field, { field = it }, BR.myField) {
* doSomething(it)
* }
* ```
* */
inline fun <reified T> ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, fieldId: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
notifyPropertyChanged(fieldId)
afterChanged(new)
}
}
inline fun <reified T> ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, vararg fieldIds: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
fieldIds.forEach { notifyPropertyChanged(it) }
afterChanged(new)
}
}
@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
abstract class RvItem {
abstract val layoutRes: Int
}
interface ItemWrapper<E> {
val item: E
}
interface ViewAwareItem {
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
}
interface DiffItem<T : Any> {
fun itemSameAs(other: T): Boolean {
if (this === other) return true
return when (this) {
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
else -> this == other
}
}
fun contentSameAs(other: T) = true
}
@@ -1,121 +0,0 @@
package com.topjohnwu.magisk.databinding
import android.annotation.SuppressLint
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.BindingAdapter
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableList
import androidx.databinding.ObservableList.OnListChangedCallback
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.BR
class RvItemAdapter<T: RvItem>(
val items: List<T>,
val extraBindings: SparseArray<*>?
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
private var lifecycleOwner: LifecycleOwner? = null
private var recyclerView: RecyclerView? = null
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
override fun onAttachedToRecyclerView(rv: RecyclerView) {
lifecycleOwner = rv.findViewTreeLifecycleOwner()
recyclerView = rv
if (items is ObservableList)
items.addOnListChangedCallback(observer)
}
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
lifecycleOwner = null
recyclerView = null
if (items is ObservableList)
items.removeOnListChangedCallback(observer)
}
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
val inflator = LayoutInflater.from(parent.context)
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.binding.setVariable(BR.item, item)
extraBindings?.let {
for (i in 0 until it.size()) {
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
}
}
holder.binding.lifecycleOwner = lifecycleOwner
holder.binding.executePendingBindings()
recyclerView?.let {
if (item is ViewAwareItem)
item.onBind(holder.binding, it)
}
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].layoutRes
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
@SuppressLint("NotifyDataSetChanged")
override fun onChanged(sender: ObservableList<T>) {
notifyDataSetChanged()
}
override fun onItemRangeChanged(
sender: ObservableList<T>,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(
sender: ObservableList<T>?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList<T>?,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
for (i in 0 until itemCount) {
notifyItemMoved(fromPosition + i, toPosition + i)
}
}
override fun onItemRangeRemoved(
sender: ObservableList<T>?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeRemoved(positionStart, itemCount)
}
}
}
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
@BindingAdapter("items", "extraBindings", requireAll = false)
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
if (items != null) {
val rva = (adapter as? RvItemAdapter<*>)
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
adapter = RvItemAdapter(items, extraBindings)
}
}
}
@@ -1,17 +1,26 @@
package com.topjohnwu.magisk.dialog
import android.view.LayoutInflater
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import top.yukonga.miuix.kmp.basic.Text
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
@@ -21,19 +30,40 @@ abstract class MarkDownDialog : DialogBuilder {
@CallSuper
override fun build(dialog: MagiskDialog) {
with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
setView(view)
val tv = view.findViewById<TextView>(R.id.md_txt)
activity.lifecycleScope.launch {
try {
val text = withContext(Dispatchers.IO) { getMarkdownText() }
ServiceLocator.markwon.setMarkdown(tv, text)
} catch (e: IOException) {
Timber.e(e)
tv.setText(CoreR.string.download_file_error)
}
}
dialog.setView {
MarkdownContent(::getMarkdownText)
}
}
}
@Composable
private fun MarkdownContent(getMarkdownText: suspend () -> String) {
var mdText by remember { mutableStateOf<String?>(null) }
var error by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
try {
mdText = withContext(Dispatchers.IO) { getMarkdownText() }
} catch (e: IOException) {
Timber.e(e)
error = true
}
}
when {
error -> Text(stringResource(CoreR.string.download_file_error))
mdText != null -> {
val text = mdText!!
AndroidView(
factory = { context ->
TextView(context).apply {
ServiceLocator.markwon.setMarkdown(this, text)
}
},
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
)
}
}
}
@@ -31,7 +31,7 @@ class UninstallDialog : DialogBuilder {
}
@Suppress("DEPRECATION")
private fun restore(activity: UIActivity<*>) {
private fun restore(activity: UIActivity) {
val dialog = ProgressDialog(activity).apply {
setMessage(activity.getString(R.string.restore_img_msg))
show()
@@ -19,24 +19,24 @@ class PermissionEvent(
private val callback: (Boolean) -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) =
override fun invoke(activity: UIActivity) =
activity.withPermission(permission, callback)
}
class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.onBackPressed()
}
}
class DieEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.finish()
}
}
class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.relaunch()
}
}
@@ -45,7 +45,7 @@ class AuthEvent(
private val callback: () -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.withAuthentication { if (it) callback() }
}
}
@@ -54,7 +54,7 @@ class GetContentEvent(
private val type: String,
private val callback: ContentResultCallback
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.getContent(type, callback)
}
}
@@ -83,7 +83,7 @@ class SnackbarEvent(
builder: Snackbar.() -> Unit = {}
) : this(msg.asText(), length, builder)
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
activity.showSnackbar(msg.getText(activity.resources), length, builder)
}
}
@@ -91,7 +91,7 @@ class SnackbarEvent(
class DialogEvent(
private val builder: DialogBuilder
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
override fun invoke(activity: UIActivity) {
MagiskDialog(activity).apply(builder::build).show()
}
}
@@ -17,7 +17,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
@@ -55,6 +54,7 @@ import com.topjohnwu.magisk.ui.navigation.rememberNavigator
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MagiskDialogHost
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -63,9 +63,8 @@ import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel()
class MainActivity : UIActivity<ViewDataBinding>(), SplashScreenHost {
class MainActivity : UIActivity(), SplashScreenHost {
override val layoutRes = 0
override val viewModel by viewModel<MainViewModel>()
override val splashController = SplashController(this)
override val snackbarView: View
@@ -103,6 +102,7 @@ class MainActivity : UIActivity<ViewDataBinding>(), SplashScreenHost {
setContent {
MagiskTheme {
MagiskDialogHost()
val navigator = rememberNavigator(Route.Main)
CompositionLocalProvider(LocalNavigator provides navigator) {
HandleFlashIntent(navigator)
@@ -15,7 +15,7 @@ import com.topjohnwu.magisk.arch.ViewEvent
fun ObserveViewEvents(viewModel: BaseViewModel) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val activity = context as? UIActivity<*>
val activity = context as? UIActivity
DisposableEffect(viewModel, lifecycleOwner) {
val observer = { event: ViewEvent ->
@@ -9,7 +9,6 @@ import android.view.View
import android.view.Window
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
@@ -23,9 +22,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity<ViewDataBinding>(), UntrackedActivity {
open class SuRequestActivity : UIActivity(), UntrackedActivity {
override val layoutRes: Int = 0
override val viewModel: SuRequestViewModel by viewModel()
override val snackbarView: View
@@ -1,216 +1,103 @@
package com.topjohnwu.magisk.view
import android.app.Activity
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.databinding.setAdapter
import com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.extra.SuperDialog
import top.yukonga.miuix.kmp.theme.MiuixTheme
typealias DialogButtonClickListener = (DialogInterface) -> Unit
class MagiskDialog(val context: Activity) {
class MagiskDialog(
context: Activity, theme: Int = 0
) : AppCompatDialog(context, theme) {
val activity: UIActivity get() = context as UIActivity
val ownerActivity: Activity get() = context
val showState = mutableStateOf(true)
private val binding: DialogMagiskBaseBinding =
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
private val data = Data()
internal var title by mutableStateOf<CharSequence>("")
internal var message by mutableStateOf<CharSequence>("")
internal var icon by mutableStateOf<Drawable?>(null)
internal var isCancelable by mutableStateOf(true)
internal var customContent by mutableStateOf<(@Composable () -> Unit)?>(null)
val activity: UIActivity<*> get() = ownerActivity as UIActivity<*>
init {
binding.setVariable(BR.data, data)
setCancelable(true)
setOwnerActivity(context)
}
inner class Data : ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
var icon: Drawable? = null
set(value) = set(value, field, { field = it }, BR.icon)
@get:Bindable
var title: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.title)
@get:Bindable
var message: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.message)
val buttonPositive = ButtonViewModel()
val buttonNeutral = ButtonViewModel()
val buttonNegative = ButtonViewModel()
}
enum class ButtonType {
POSITIVE, NEUTRAL, NEGATIVE
}
interface Button {
var icon: Int
var text: Any
var isEnabled: Boolean
var doNotDismiss: Boolean
fun onClick(listener: DialogButtonClickListener)
}
inner class ButtonViewModel : Button, ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
override var icon = 0
set(value) = set(value, field, { field = it }, BR.icon, BR.gone)
@get:Bindable
var message: String = ""
set(value) = set(value, field, { field = it }, BR.message, BR.gone)
override var text: Any
get() = message
set(value) {
message = when (value) {
is Int -> context.getText(value)
else -> value
}.toString()
}
@get:Bindable
val gone get() = icon == 0 && message.isEmpty()
@get:Bindable
override var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled)
override var doNotDismiss = false
private var onClickAction: DialogButtonClickListener = {}
override fun onClick(listener: DialogButtonClickListener) {
onClickAction = listener
}
fun clicked() {
onClickAction(this@MagiskDialog)
if (!doNotDismiss) {
dismiss()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
super.setContentView(binding.root)
val default = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, javaClass.canonicalName)
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, default)
val materialShapeDrawable = MaterialShapeDrawable(context, null, androidx.appcompat.R.attr.alertDialogStyle, com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents)
materialShapeDrawable.initializeElevationOverlay(context)
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
val inset = context.resources.getDimensionPixelSize(com.google.android.material.R.dimen.appcompat_dialog_background_inset)
window?.apply {
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun setTitle(@StringRes titleId: Int) { data.title = context.getString(titleId) }
override fun setTitle(title: CharSequence?) { data.title = title ?: "" }
val buttonPositive = ButtonState()
val buttonNeutral = ButtonState()
val buttonNegative = ButtonState()
fun setTitle(@StringRes titleId: Int) { title = context.getString(titleId) }
fun setTitle(title: CharSequence?) { this.title = title ?: "" }
fun setMessage(@StringRes msgId: Int, vararg args: Any) {
data.message = context.getString(msgId, *args)
message = context.getString(msgId, *args)
}
fun setMessage(message: CharSequence) { data.message = message }
fun setMessage(message: CharSequence) { this.message = message }
fun setIcon(@DrawableRes drawableRes: Int) {
data.icon = AppCompatResources.getDrawable(context, drawableRes)
icon = AppCompatResources.getDrawable(context, drawableRes)
}
fun setIcon(drawable: Drawable) { data.icon = drawable }
fun setIcon(drawable: Drawable) { icon = drawable }
fun setCancelable(cancelable: Boolean) { isCancelable = cancelable }
fun setButton(buttonType: ButtonType, builder: Button.() -> Unit) {
val button = when (buttonType) {
ButtonType.POSITIVE -> data.buttonPositive
ButtonType.NEUTRAL -> data.buttonNeutral
ButtonType.NEGATIVE -> data.buttonNegative
}
button.apply(builder)
when (buttonType) {
ButtonType.POSITIVE -> buttonPositive
ButtonType.NEUTRAL -> buttonNeutral
ButtonType.NEGATIVE -> buttonNegative
}.apply(builder)
}
class DialogItem(
override val item: CharSequence,
val position: Int
) : RvItem(), DiffItem<DialogItem>, ItemWrapper<CharSequence> {
override val layoutRes = R.layout.item_list_single_line
}
fun interface DialogClickListener {
fun onClick(position: Int)
fun setView(content: @Composable () -> Unit) {
customContent = content
}
fun setListItems(
list: Array<out CharSequence>,
listener: DialogClickListener
) = setView(
RecyclerView(context).also {
it.isNestedScrollingEnabled = false
it.layoutManager = LinearLayoutManager(context)
val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
val extraBindings = bindExtra { sa ->
sa.put(BR.listener, DialogClickListener { pos ->
listener.onClick(pos)
dismiss()
})
) {
customContent = {
Column {
list.forEachIndexed { index, text ->
Text(
text = text.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable {
listener.onClick(index)
dismiss()
}
.padding(horizontal = 16.dp, vertical = 12.dp),
style = MiuixTheme.textStyles.body1,
)
}
}
it.setAdapter(items, extraBindings)
}
)
fun setView(view: View) {
binding.dialogBaseContainer.removeAllViews()
binding.dialogBaseContainer.addView(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
fun resetButtons() {
ButtonType.values().forEach {
ButtonType.entries.forEach {
setButton(it) {
text = ""
icon = 0
@@ -221,12 +108,136 @@ class MagiskDialog(
}
}
// Prevent calling setContentView
fun show() { DialogManager.show(this) }
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {}
fun dismiss() {
showState.value = false
DialogManager.dismiss(this)
}
enum class ButtonType { POSITIVE, NEUTRAL, NEGATIVE }
interface Button {
var icon: Int
var text: Any
var isEnabled: Boolean
var doNotDismiss: Boolean
fun onClick(listener: () -> Unit)
}
inner class ButtonState : Button {
var label by mutableStateOf("")
var iconRes by mutableIntStateOf(0)
override var isEnabled by mutableStateOf(true)
override var doNotDismiss = false
internal var onClickAction: () -> Unit = {}
val isVisible get() = iconRes != 0 || label.isNotEmpty()
override var icon: Int
get() = iconRes
set(value) { iconRes = value }
override var text: Any
get() = label
set(value) {
label = when (value) {
is Int -> context.getText(value)
else -> value
}.toString()
}
override fun onClick(listener: () -> Unit) {
onClickAction = listener
}
fun performClick() {
onClickAction()
if (!doNotDismiss) dismiss()
}
}
fun interface DialogClickListener {
fun onClick(position: Int)
}
}
object DialogManager {
val dialogs = mutableStateListOf<MagiskDialog>()
fun show(dialog: MagiskDialog) { dialogs.add(dialog) }
fun dismiss(dialog: MagiskDialog) { dialogs.remove(dialog) }
}
@Composable
fun MagiskDialogHost() {
for (dialog in DialogManager.dialogs.toList()) {
MagiskDialogContent(dialog)
}
}
@Composable
private fun MagiskDialogContent(dialog: MagiskDialog) {
SuperDialog(
title = dialog.title.toString(),
show = dialog.showState,
onDismissRequest = {
if (dialog.isCancelable) dialog.dismiss()
}
) {
dialog.icon?.let { iconDrawable ->
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Image(
painter = rememberDrawablePainter(iconDrawable),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
}
Spacer(Modifier.height(8.dp))
}
if (dialog.message.isNotEmpty()) {
Text(
text = dialog.message.toString(),
style = MiuixTheme.textStyles.body1,
modifier = Modifier.fillMaxWidth()
)
}
dialog.customContent?.invoke()
val hasButtons = dialog.buttonNeutral.isVisible ||
dialog.buttonNegative.isVisible ||
dialog.buttonPositive.isVisible
if (hasButtons) {
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
if (dialog.buttonNeutral.isVisible) {
TextButton(
text = dialog.buttonNeutral.label,
enabled = dialog.buttonNeutral.isEnabled,
onClick = { dialog.buttonNeutral.performClick() }
)
}
Spacer(Modifier.weight(1f))
if (dialog.buttonNegative.isVisible) {
TextButton(
text = dialog.buttonNegative.label,
enabled = dialog.buttonNegative.isEnabled,
onClick = { dialog.buttonNegative.performClick() }
)
}
if (dialog.buttonPositive.isVisible) {
Spacer(Modifier.width(8.dp))
TextButton(
text = dialog.buttonPositive.label,
enabled = dialog.buttonPositive.isEnabled,
onClick = { dialog.buttonPositive.performClick() }
)
}
}
}
}
}
@@ -1,180 +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="data"
type="com.topjohnwu.magisk.view.MagiskDialog.Data" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:layout_width="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<ImageView
android:id="@+id/dialog_base_icon"
style="@style/WidgetFoundation.Image.Big"
gone="@{data.icon == null}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/l1"
android:src="@{data.icon}"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_delete_md2" />
<TextView
android:id="@+id/dialog_base_title"
gone="@{data.title.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:gravity="center"
android:text="@{data.title}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l_50"
android:overScrollMode="ifContentScrolls"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_space"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/dialog_base_message"
gone="@{data.message.length == 0}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@{data.message}"
android:textAppearance="@style/AppearanceFoundation.Body"
tools:lines="3"
tools:text="@tools:sample/lorem/random" />
<FrameLayout
android:id="@+id/dialog_base_container"
gone="@{data.message.length != 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
</androidx.core.widget.NestedScrollView>
<Space
android:id="@+id/dialog_base_space"
android:layout_width="wrap_content"
android:layout_height="@dimen/l_50"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.ButtonBarLayout
android:id="@+id/dialog_base_buttons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="bottom|center_horizontal"
android:layoutDirection="locale"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/dialog_base_button_2"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNeutral.gone}"
isEnabled="@{data.buttonNeutral.isEnabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonNeutral.isEnabled}"
android:focusable="@{data.buttonNeutral.isEnabled}"
android:onClick="@{() -> data.buttonNeutral.clicked()}"
android:text="@{data.buttonNeutral.message}"
app:icon="@{data.buttonNeutral.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<Button
android:id="@+id/dialog_base_button_3"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNegative.gone}"
isEnabled="@{data.buttonNegative.isEnabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonNegative.isEnabled}"
android:focusable="@{data.buttonNegative.isEnabled}"
android:onClick="@{() -> data.buttonNegative.clicked()}"
android:text="@{data.buttonNegative.message}"
app:icon="@{data.buttonNegative.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Button
android:id="@+id/dialog_base_button_1"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonPositive.gone}"
isEnabled="@{data.buttonPositive.isEnabled}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonPositive.isEnabled}"
android:focusable="@{data.buttonPositive.isEnabled}"
android:onClick="@{() -> data.buttonPositive.clicked()}"
android:text="@{data.buttonPositive.message}"
app:icon="@{data.buttonPositive.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
</androidx.appcompat.widget.ButtonBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.view.MagiskDialog.DialogItem" />
<variable
name="listener"
type="com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener" />
</data>
<TextView
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> listener.onClick(item.position)}"
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l_75"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@dimen/l_75"
android:text="@{item.item}"
android:textAppearance="@style/AppearanceFoundation.Body"
tools:text="@tools:sample/lorem" />
</layout>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/md_txt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:breakStrategy="simple"
android:hyphenationFrequency="none"
android:paddingTop="10dp"
android:textAppearance="@style/AppearanceFoundation.Caption"
tools:ignore="UnusedAttribute" />
</ScrollView>
-12
View File
@@ -3,7 +3,6 @@ kotlin = "2.3.0"
android = "9.0.1"
ksp = "2.3.4"
rikka = "1.3.0"
navigation = "2.9.7"
libsu = "6.0.0"
okhttp = "5.3.2"
retrofit = "3.0.0"
@@ -33,17 +32,10 @@ activity = { module = "androidx.activity:activity", version = "1.12.4" }
appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" }
core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" }
core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version = "1.8.9" }
navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version = "1.4.1" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.4.0" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.2.0" }
transition = { module = "androidx.transition:transition", version = "1.7.0" }
collection-ktx = { module = "androidx.collection:collection-ktx", version = "1.5.0" }
material = { module = "com.google.android.material:material", version = "1.13.0" }
jdk-libs = { module = "com.android.tools:desugar_jdk_libs_nio", version = "2.1.5" }
@@ -53,13 +45,11 @@ test-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.3.0" }
# topjohnwu
indeterminate-checkbox = { module = "com.github.topjohnwu:indeterminate-checkbox", version = "1.0.7" }
libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" }
libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" }
# Rikka
rikka-recyclerview = { module = "dev.rikka.rikkax.recyclerview:recyclerview-ktx", version = "1.3.2" }
rikka-layoutinflater = { module = "dev.rikka.rikkax.layoutinflater:layoutinflater", version.ref = "rikka" }
rikka-insets = { module = "dev.rikka.rikkax.insets:insets", version.ref = "rikka" }
@@ -85,5 +75,3 @@ android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
lsparanoid = { id = "org.lsposed.lsparanoid", version = "0.6.0" }
moshix = { id = "dev.zacsweers.moshix", version = "0.34.4" }
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "android" }
navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }