Migrate SuRequestActivity to Compose and clean up dead View code

Rewrite SuRequestActivity and SuRequestViewModel to use Compose state
and miuix components, preserving tapjack protection and security logic.
Extract rememberDrawablePainter to shared utility. Remove dead code:
BaseFragment, NavigationActivity, ShowUIEvent, FragmentExecutor, and
UIActivity.setContentView(). Fix bottom nav icons to use outlined
vector drawables instead of animated selectors.

Made-with: Cursor
This commit is contained in:
LoveSy
2026-03-03 13:41:36 +08:00
committed by topjohnwu
parent 2cab7d6c7b
commit d7ec7f826b
13 changed files with 244 additions and 434 deletions
@@ -1,96 +0,0 @@
package com.topjohnwu.magisk.arch
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
val activity get() = getActivity() as? NavigationActivity<*>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
private val navigation get() = activity?.navigation
open val snackbarView: View? get() = null
open val snackbarAnchorView: View? get() = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = viewLifecycleOwner
}
if (this is MenuProvider) {
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
}
savedInstanceState?.let { viewModel.onRestoreState(it) }
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
viewModel.onSaveState(outState)
}
override fun onStart() {
super.onStart()
activity?.supportActionBar?.subtitle = null
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> activity?.let { event(it) } ?: Unit
is FragmentExecutor -> event(this)
else -> Unit
}
open fun onKeyEvent(event: KeyEvent): Boolean {
return false
}
open fun onBackPressed(): Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
this@BaseFragment.onPreBind(binding)
return true
}
})
}
override fun onResume() {
super.onResume()
viewModel.let {
if (it is AsyncLoadViewModel)
it.startLoading()
}
}
protected open fun onPreBind(binding: Binding) {
(binding.root as? ViewGroup)?.startAnimations()
}
fun NavDirections.navigate() {
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
}
}
@@ -1,50 +0,0 @@
package com.topjohnwu.magisk.arch
import android.content.ContentResolver
import android.view.KeyEvent
import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navOptions
import com.topjohnwu.magisk.utils.AccessibilityUtils
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
abstract val navHostId: Int
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
}
protected val currentFragment get() =
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
val navigation: NavController get() = navHostFragment.navController
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
}
override fun onBackPressed() {
if (binded) {
if (currentFragment?.onBackPressed() == false) {
super.onBackPressed()
}
}
}
companion object {
fun navigate(directions: NavDirections, navigation: NavController, cr: ContentResolver) {
if (AccessibilityUtils.isAnimationEnabled(cr)) {
navigation.navigate(directions)
} else {
navigation.navigate(directions, navOptions {})
}
}
}
fun NavDirections.navigate() {
navigate(this, navigation, contentResolver)
}
}
@@ -11,13 +11,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.ActivityExtension
@@ -96,17 +94,6 @@ abstract class UIActivity<Binding : ViewDataBinding>
extension.onSaveInstanceState(outState)
}
fun setContentView() {
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
}
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
binding.root.rootView.accessibilityDelegate = delegate
}
fun showSnackbar(
message: CharSequence,
length: Int = Snackbar.LENGTH_SHORT,
@@ -16,6 +16,3 @@ interface ActivityExecutor {
operator fun invoke(activity: UIActivity<*>)
}
interface FragmentExecutor {
operator fun invoke(fragment: BaseFragment<*>)
}
@@ -1,7 +1,6 @@
package com.topjohnwu.magisk.events
import android.content.Context
import android.view.View
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor
@@ -36,14 +35,6 @@ class DieEvent : ViewEvent(), ActivityExecutor {
}
}
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.setContentView()
activity.setAccessibilityDelegate(delegate)
}
}
class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.relaunch()
@@ -50,11 +50,11 @@ import top.yukonga.miuix.kmp.basic.NavigationItem
import com.topjohnwu.magisk.core.R as CoreR
enum class Tab(val titleRes: Int, val iconRes: Int) {
HOME(CoreR.string.section_home, R.drawable.ic_home_md2),
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_md2),
LOG(CoreR.string.logs, R.drawable.ic_bug_md2),
MODULES(CoreR.string.modules, R.drawable.ic_module_md2),
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_md2);
HOME(CoreR.string.section_home, R.drawable.ic_home_outlined_md2),
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_outlined_md2),
LOG(CoreR.string.logs, R.drawable.ic_bug_outlined_md2),
MODULES(CoreR.string.modules, R.drawable.ic_module_outlined_md2),
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_outlined_md2);
}
@Composable
@@ -1,8 +1,6 @@
package com.topjohnwu.magisk.ui.deny
import androidx.compose.animation.AnimatedVisibility
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -25,10 +23,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
@@ -255,15 +251,3 @@ private fun ProcessRow(proc: DenyProcessState) {
}
}
@Composable
private fun rememberDrawablePainter(drawable: Drawable): Painter {
return remember(drawable) {
val w = drawable.intrinsicWidth.coerceAtLeast(1)
val h = drawable.intrinsicHeight.coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
drawable.draw(canvas)
BitmapPainter(bitmap.asImageBitmap())
}
}
@@ -1,7 +1,5 @@
package com.topjohnwu.magisk.ui.superuser
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@@ -25,9 +23,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.core.model.su.SuPolicy
@@ -213,15 +209,3 @@ private fun PolicySlider(value: Int, onValueChange: (Int) -> Unit) {
}
}
@Composable
private fun rememberDrawablePainter(drawable: Drawable): Painter {
return remember(drawable) {
val w = drawable.intrinsicWidth.coerceAtLeast(1)
val h = drawable.intrinsicHeight.coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
drawable.draw(canvas)
BitmapPainter(bitmap.asImageBitmap())
}
}
@@ -5,8 +5,11 @@ import android.content.pm.ActivityInfo
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
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
@@ -14,17 +17,20 @@ import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity<ActivityRequestBinding>(), UntrackedActivity {
open class SuRequestActivity : UIActivity<ViewDataBinding>(), UntrackedActivity {
override val layoutRes: Int = R.layout.activity_request
override val layoutRes: Int = 0
override val viewModel: SuRequestViewModel by viewModel()
override val snackbarView: View
get() = window.decorView.findViewById(android.R.id.content)
override fun onCreate(savedInstanceState: Bundle?) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
@@ -51,6 +57,17 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>(), UntrackedAc
} else {
finish()
}
if (viewModel.useTapjackProtection) {
window.decorView.rootView.accessibilityDelegate =
SuRequestViewModel.EmptyAccessibilityDelegate
}
setContent {
MagiskTheme {
SuRequestScreen(viewModel = viewModel)
}
}
}
override fun getTheme(): Resources.Theme {
@@ -59,6 +76,7 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>(), UntrackedAc
return theme
}
@Deprecated("Use OnBackPressedDispatcher")
override fun onBackPressed() {
viewModel.denyPressed()
}
@@ -0,0 +1,175 @@
package com.topjohnwu.magisk.ui.surequest
import android.view.MotionEvent
import android.widget.Toast
import androidx.compose.foundation.Image
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.fillMaxSize
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.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.R as CoreR
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.extra.SuperDropdown
import top.yukonga.miuix.kmp.theme.MiuixTheme
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SuRequestScreen(viewModel: SuRequestViewModel) {
if (!viewModel.showUi) return
val context = LocalContext.current
val icon = viewModel.icon
val title = viewModel.title
val packageName = viewModel.packageName
val grantEnabled = viewModel.grantEnabled
val denyCountdown = viewModel.denyCountdown
val selectedPosition = viewModel.selectedItemPosition
val timeoutEntries = stringArrayResource(CoreR.array.allow_timeout).toList()
val denyText = if (denyCountdown > 0) {
"${stringResource(CoreR.string.deny)} ($denyCountdown)"
} else {
stringResource(CoreR.string.deny)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.widthIn(min = 300.dp, max = 380.dp)
.padding(24.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(CoreR.string.su_request_title),
style = MiuixTheme.textStyles.headline2,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp)
) {
if (icon != null) {
Image(
painter = rememberDrawablePainter(icon),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(12.dp))
}
Column {
Text(
text = title,
style = MiuixTheme.textStyles.body1,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = packageName,
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(Modifier.height(16.dp))
SuperDropdown(
title = stringResource(CoreR.string.request_timeout),
items = timeoutEntries,
selectedIndex = selectedPosition,
onSelectedIndexChange = { index ->
viewModel.spinnerTouched()
viewModel.selectedItemPosition = index
},
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(CoreR.string.su_warning),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(
text = denyText,
onClick = { viewModel.denyPressed() },
modifier = Modifier.weight(1f)
)
TextButton(
text = stringResource(CoreR.string.grant),
enabled = grantEnabled,
onClick = { viewModel.grantPressed() },
modifier = Modifier
.weight(1f)
.then(
if (viewModel.useTapjackProtection) {
Modifier.pointerInteropFilter { event ->
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 ||
event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
) {
if (event.action == MotionEvent.ACTION_UP) {
context.toast(
CoreR.string.touch_filtered_warning,
Toast.LENGTH_SHORT
)
}
true
} else {
false
}
}
} else Modifier
)
)
}
}
}
}
}
@@ -1,9 +1,7 @@
package com.topjohnwu.magisk.ui.surequest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.CountDownTimer
@@ -14,9 +12,11 @@ import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
@@ -27,11 +27,8 @@ import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.DieEvent
import com.topjohnwu.magisk.events.ShowUIEvent
import com.topjohnwu.magisk.utils.TextHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit.SECONDS
@@ -41,33 +38,16 @@ class SuRequestViewModel(
private val timeoutPrefs: SharedPreferences
) : BaseViewModel() {
lateinit var icon: Drawable
lateinit var title: String
lateinit var packageName: String
var icon by mutableStateOf<Drawable?>(null)
var title by mutableStateOf("")
var packageName by mutableStateOf("")
@get:Bindable
val denyText = DenyText()
var selectedItemPosition by mutableIntStateOf(0)
var grantEnabled by mutableStateOf(false)
var denyCountdown by mutableIntStateOf(0)
@get:Bindable
var selectedItemPosition = 0
set(value) = set(value, field, { field = it }, BR.selectedItemPosition)
@get:Bindable
var grantEnabled = false
set(value) = set(value, field, { field = it }, BR.grantEnabled)
@SuppressLint("ClickableViewAccessibility")
val grantTouchListener = View.OnTouchListener { _: View, event: MotionEvent ->
// Filter obscured touches by consuming them.
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
if (event.action == MotionEvent.ACTION_UP) {
AppContext.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
}
return@OnTouchListener Config.suTapjack
}
false
}
var showUi by mutableStateOf(false)
var useTapjackProtection by mutableStateOf(false)
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
@@ -87,9 +67,8 @@ class SuRequestViewModel(
respond(DENY)
}
fun spinnerTouched(): Boolean {
fun spinnerTouched() {
cancelTimer()
return false
}
fun handleRequest(intent: Intent) {
@@ -107,8 +86,6 @@ class SuRequestViewModel(
val app = info.applicationInfo
if (app == null) {
// The request is not coming from an app process, and the UID is a
// shared UID. We have no way to know where this request comes from.
icon = pm.defaultActivityIcon
title = "[SharedUID] ${info.sharedUserId}"
packageName = info.sharedUserId.toString()
@@ -120,21 +97,14 @@ class SuRequestViewModel(
}
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
// Set timer
timer.start()
// Actually show the UI
ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
useTapjackProtection = Config.suTapjack
showUi = true
initialized = true
}
private fun respond(action: Int) {
if (!initialized) {
// ignore the response until showDialog done
return
}
if (!initialized) return
timer.cancel()
val pos = selectedItemPosition
@@ -142,14 +112,13 @@ class SuRequestViewModel(
viewModelScope.launch {
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])
// Kill activity after response
DieEvent().publish()
}
}
private fun cancelTimer() {
timer.cancel()
denyText.seconds = 0
denyCountdown = 0
}
private inner class SuTimer(
@@ -161,29 +130,15 @@ class SuRequestViewModel(
if (!grantEnabled && remains <= millis - 1000) {
grantEnabled = true
}
denyText.seconds = (remains / 1000).toInt() + 1
denyCountdown = (remains / 1000).toInt() + 1
}
override fun onFinish() {
denyText.seconds = 0
denyCountdown = 0
respond(DENY)
}
}
inner class DenyText : TextHolder() {
var seconds = 0
set(value) = set(value, field, { field = it }, BR.denyText)
override fun getText(resources: Resources): CharSequence {
return if (seconds != 0)
"${resources.getString(R.string.deny)} ($seconds)"
else
resources.getString(R.string.deny)
}
}
// Invisible for accessibility services
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
@@ -0,0 +1,22 @@
package com.topjohnwu.magisk.ui.util
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
@Composable
fun rememberDrawablePainter(drawable: Drawable): Painter {
return remember(drawable) {
val w = drawable.intrinsicWidth.coerceAtLeast(1)
val h = drawable.intrinsicHeight.coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(bitmap)
drawable.setBounds(0, 0, w, h)
drawable.draw(canvas)
BitmapPainter(bitmap.asImageBitmap())
}
}
@@ -1,157 +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="viewModel"
type="com.topjohnwu.magisk.ui.surequest.SuRequestViewModel" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<LinearLayout
android:id="@+id/su_popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="350dp"
android:orientation="vertical">
<TextView
android:id="@+id/request_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/l1"
android:layout_marginBottom="@dimen/l_50"
android:gravity="center_horizontal"
android:text="@string/su_request_title"
android:textAppearance="@style/AppearanceFoundation.Title" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/l_50"
android:layout_marginBottom="@dimen/l_50"
android:orientation="horizontal"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<ImageView
android:id="@+id/app_icon"
style="@style/WidgetFoundation.Icon"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/l_50"
android:layout_marginEnd="@dimen/l1"
android:layout_weight="0"
android:padding="0dp"
android:src="@{viewModel.icon}"
app:tint="@null"
tools:src="@drawable/ic_delete_md2" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxWidth="300dp"
android:maxLines="1"
android:minWidth="200dp"
android:text="@{viewModel.title}"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textStyle="bold"
tools:text="Magisk" />
<TextView
android:id="@+id/package_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxWidth="300dp"
android:maxLines="1"
android:minWidth="200dp"
android:text="@{viewModel.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
tools:text="com.topjohnwu.magisk" />
</LinearLayout>
</LinearLayout>
<Spinner
android:id="@+id/timeout"
onTouch="@{() -> viewModel.spinnerTouched()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:enabled="@{viewModel.grantEnabled}"
app:items="@{@stringArray/allow_timeout}"
app:layout="@{@layout/item_spinner}"
android:selection="@={viewModel.selectedItemPosition}" />
<TextView
android:id="@+id/warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="@dimen/l1"
android:gravity="center"
android:text="@string/su_warning"
android:textAppearance="@style/AppearanceFoundation.Body"
android:textColor="?colorError"
android:textStyle="bold"
tools:text="@string/su_warning" />
<LinearLayout
style="?android:buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="horizontal"
android:paddingStart="@dimen/l2"
android:paddingEnd="@dimen/l2">
<Button
android:id="@+id/deny_btn"
style="@style/WidgetFoundation.Button.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="@{() -> viewModel.denyPressed()}"
android:text="@{viewModel.denyText}"
android:textColor="?colorOnSurfaceVariant"
tools:text="@string/deny" />
<Button
android:id="@+id/grant_btn"
style="@style/WidgetFoundation.Button.Text"
onTouch="@{viewModel.grantTouchListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="@{() -> viewModel.grantPressed()}"
android:enabled="@{viewModel.grantEnabled}"
android:text="@string/grant" />
<requestFocus />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>