mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-07-03 14:08:39 +02:00
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:
@@ -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>
|
||||
Reference in New Issue
Block a user