manager: Update WebUI X to the latest version (#345)

* manager: bump WebUI X

* manager: forgot to add interface name back

* manager: use compose set content for webuix

* manager: fix :jna library and reflections in prod
This commit is contained in:
Der_Googler
2025-08-21 01:21:43 +02:00
committed by GitHub
parent 770ed1fdf2
commit c5ed6e1e8c
13 changed files with 511 additions and 425 deletions

View File

@@ -28,12 +28,12 @@ apksign {
android {
/**signingConfigs {
create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = ""
keyAlias = ""
keyPassword = ""
}
create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = ""
keyAlias = ""
keyPassword = ""
}
}**/
namespace = "com.sukisu.ultra"
@@ -41,10 +41,13 @@ android {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
/**debug {
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
}**/
}
@@ -131,6 +134,8 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp)
@@ -159,7 +164,16 @@ dependencies {
implementation(libs.mmrl.platform)
compileOnly(libs.mmrl.hidden.api)
implementation(libs.mmrl.webui)
/**
* Compile only `Java-Native-Access` since plugins are disabled in both WebUI X and KSU WebUI
* Avoid using:
* - fun WXInterface.registerLibrary(clazz: Class<*>, name: String): Unit
* - fun WXInterface.unregisterLibrary(clazz: Class<*>): Unit
* - fun WXInterface.isLibraryRegistered(libName: String): Boolean
*/
compileOnly(libs.mmrl.webui.jna)
implementation(libs.mmrl.webui.portable)
implementation(libs.mmrl.ui)
implementation(libs.accompanist.drawablepainter)

View File

@@ -4,43 +4,14 @@
-dontwarn org.conscrypt.**
-dontwarn kotlinx.serialization.**
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn com.google.auto.service.AutoService
-dontwarn com.google.j2objc.annotations.RetainedWith
-dontwarn javax.lang.model.SourceVersion
-dontwarn javax.lang.model.element.AnnotationMirror
-dontwarn javax.lang.model.element.AnnotationValue
-dontwarn javax.lang.model.element.Element
-dontwarn javax.lang.model.element.ElementKind
-dontwarn javax.lang.model.element.ElementVisitor
-dontwarn javax.lang.model.element.ExecutableElement
-dontwarn javax.lang.model.element.Modifier
-dontwarn javax.lang.model.element.Name
-dontwarn javax.lang.model.element.PackageElement
-dontwarn javax.lang.model.element.TypeElement
-dontwarn javax.lang.model.element.TypeParameterElement
-dontwarn javax.lang.model.element.VariableElement
-dontwarn javax.lang.model.type.ArrayType
-dontwarn javax.lang.model.type.DeclaredType
-dontwarn javax.lang.model.type.ExecutableType
-dontwarn javax.lang.model.type.TypeKind
-dontwarn javax.lang.model.type.TypeMirror
-dontwarn javax.lang.model.type.TypeVariable
-dontwarn javax.lang.model.type.TypeVisitor
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
-dontwarn javax.lang.model.util.ElementFilter
-dontwarn javax.lang.model.util.Elements
-dontwarn javax.lang.model.util.SimpleElementVisitor8
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
-dontwarn javax.lang.model.util.Types
-dontwarn javax.tools.Diagnostic$Kind
# MMRL:webui reflection
-keep class com.dergoogler.mmrl.webui.model.ModId { *; }
-keep class androidx.compose.ui.graphics.Color { *; }
-keep class androidx.compose.material3.ButtonColors { *; }
-keep class androidx.compose.material3.CardColors { *; }
-keep class androidx.compose.material3.ColorScheme { *; }
-keep class com.dergoogler.mmrl.platform.model.ModId { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.WXOptions { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.WXInterface { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }

View File

@@ -11,7 +11,7 @@ import android.os.Build
import android.os.Bundle
import coil.Coil
import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.PlatformManager
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File
@@ -90,7 +90,7 @@ class KernelSUApplication : Application() {
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
Platform.setHiddenApiExemptions()
PlatformManager.setHiddenApiExemptions()
val context = this
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)

View File

@@ -84,8 +84,8 @@ import java.util.concurrent.TimeUnit
import androidx.core.content.edit
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.webui.WebUIXActivity
import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri
import com.dergoogler.mmrl.platform.PlatformManager
import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.component.AnimatedFab
@@ -460,7 +460,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
"wx" -> wxEngine
"ksu" -> ksuEngine
else -> {
if (Platform.isAlive) {
if (PlatformManager.isAlive) {
wxEngine
} else {
ksuEngine

View File

@@ -2,6 +2,7 @@ package com.sukisu.ultra.ui.theme
import android.content.ContentResolver
import android.content.Context
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.util.Log
@@ -48,8 +49,10 @@ import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalConfiguration
/**
* 主题配置对象,管理应用的主题相关状态
@@ -84,13 +87,13 @@ object ThemeConfig {
*/
@Composable
fun KernelSUTheme(
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
darkTheme: Boolean = when (ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> isSystemInDarkTheme()
},
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
content: @Composable () -> Unit
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val systemIsDark = isSystemInDarkTheme()
@@ -135,18 +138,17 @@ fun KernelSUTheme(
ThemeConfig.backgroundImageLoaded = false
}
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", true)
ThemeConfig.preventBackgroundRefresh =
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", true)
}
// 创建颜色方案
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
val colorScheme = createColorScheme(
context = context,
darkTheme = darkTheme,
dynamicColor = dynamicColor
)
// 根据暗色模式和自定义背景调整卡片配置
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
@@ -217,8 +219,18 @@ fun KernelSUTheme(
modifier = Modifier
.fillMaxSize()
.zIndex(-2f)
.background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
.background(
if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
else if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
)
)
// 自定义背景层
@@ -239,7 +251,9 @@ fun KernelSUTheme(
contentScale = ContentScale.Crop
)
.graphicsLayer {
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
alpha =
(painter.state as? AsyncImagePainter.State.Success)?.let { 1f }
?: 0f
}
)
}
@@ -288,7 +302,6 @@ fun KernelSUTheme(
* 创建动态深色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
val scheme = dynamicDarkColorScheme(context)
return scheme.copy(
@@ -303,7 +316,6 @@ private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
* 创建动态浅色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicLightColorScheme(context: Context): ColorScheme {
val scheme = dynamicLightColorScheme(context)
return scheme.copy(
@@ -314,12 +326,35 @@ private fun createDynamicLightColorScheme(context: Context): ColorScheme {
)
}
internal fun createColorScheme(
context: Context,
darkTheme: Boolean = when (ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> _isSystemInDarkTheme(context)
},
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
) = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(
context
)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
@Suppress("FunctionName")
internal fun _isSystemInDarkTheme(context: Context): Boolean {
val configuration = context.resources.configuration
val uiMode = configuration.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
/**
* 创建深色颜色方案
*/
@Composable
private fun createDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.primaryDark,
onPrimary = ThemeConfig.currentTheme.onPrimaryDark,
@@ -361,7 +396,6 @@ private fun createDarkColorScheme() = darkColorScheme(
/**
* 创建浅色颜色方案
*/
@Composable
private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.primaryLight,
onPrimary = ThemeConfig.currentTheme.onPrimaryLight,
@@ -440,7 +474,10 @@ private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
/**
* 保存并应用自定义背景
*/
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
fun Context.saveAndApplyCustomBackground(
uri: Uri,
transformation: BackgroundTransformation? = null,
) {
val finalUri = if (transformation != null) {
saveTransformedBackground(uri, transformation)
} else {
@@ -536,7 +573,7 @@ fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) {
ThemeConfig.forceDarkMode = when (mode) {
"dark" -> true
"light" -> false
else -> null

View File

@@ -7,7 +7,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.util.Log
import android.widget.Toast
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.dergoogler.mmrl.platform.PlatformManager.context
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.topjohnwu.superuser.Shell

View File

@@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.dergoogler.mmrl.platform.PlatformManager.context
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.sukisu.ultra.KernelVersion

View File

@@ -1,60 +1,57 @@
package com.sukisu.ultra.ui.webui
import android.content.Context
import android.content.ServiceConnection
import android.util.Log
import android.content.pm.PackageInfo
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.Platform.Companion.createPlatformIntent
import com.dergoogler.mmrl.platform.PlatformManager
import com.dergoogler.mmrl.platform.PlatformManager.packageManager
import com.dergoogler.mmrl.platform.PlatformManager.userManager
import com.dergoogler.mmrl.platform.model.IProvider
import com.dergoogler.mmrl.platform.model.PlatformIntent
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.Natives
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
class KsuLibSuProvider : IProvider {
override val name = "KsuLibSu"
class KsuLibSuProvider(
private val context: Context,
) : IProvider {
override val name = "SukiLibSu"
override fun isAvailable() = true
override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
override suspend fun isAuthorized() = Natives.becomeManager(context.packageName)
private val serviceIntent
get() = PlatformIntent(
ksuApp,
Platform.KsuNext,
SuService::class.java
)
private val intent by lazy {
context.createPlatformIntent<SuService>(Platform.SukiSU)
}
override fun bind(connection: ServiceConnection) {
RootService.bind(serviceIntent.intent, connection)
RootService.bind(intent, connection)
}
override fun unbind(connection: ServiceConnection) {
RootService.stop(serviceIntent.intent)
RootService.stop(intent)
}
}
// webui x
suspend fun initPlatform() = withContext(Dispatchers.IO) {
suspend fun CoroutineScope.initPlatform(context: Context = ksuApp): Deferred<Boolean> =
try {
val active = Platform.init {
this.context = ksuApp
this.platform = Platform.KsuNext
this.provider = from(KsuLibSuProvider())
val active = PlatformManager.init(this) {
from(KsuLibSuProvider(context))
}
while (!active) {
delay(1000)
}
return@withContext active
active
} catch (e: Exception) {
Log.e("KsuLibSu", "Failed to initialize platform", e)
return@withContext false
CompletableDeferred(false)
}
}
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
try {

View File

@@ -2,13 +2,13 @@ package com.sukisu.ultra.ui.webui
import android.content.Intent
import android.os.IBinder
import com.dergoogler.mmrl.platform.model.PlatformIntent.Companion.getPlatform
import com.dergoogler.mmrl.platform.Platform.Companion.getPlatform
import com.dergoogler.mmrl.platform.service.ServiceManager
import com.topjohnwu.superuser.ipc.RootService
class SuService : RootService() {
override fun onBind(intent: Intent): IBinder {
val mode = intent.getPlatform()
val mode = intent.getPlatform() ?: throw Exception("Platform not found")
return ServiceManager(mode)
}
}

View File

@@ -1,10 +1,10 @@
package com.sukisu.ultra.ui.webui
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
@@ -14,21 +14,30 @@ import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.dergoogler.mmrl.platform.model.ModId.Companion.getModId
import com.dergoogler.mmrl.platform.model.ModId.Companion.webrootDir
import com.dergoogler.mmrl.ui.component.dialog.ConfirmData
import com.dergoogler.mmrl.ui.component.dialog.confirm
import com.dergoogler.mmrl.webui.activity.WXActivity.Companion.createLoadingRenderer
import com.topjohnwu.superuser.Shell
import com.sukisu.ultra.ui.util.createRootShell
import java.io.File
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.dergoogler.mmrl.webui.view.WebUIView
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme._isSystemInDarkTheme
import com.sukisu.ultra.ui.theme.createColorScheme
import kotlinx.coroutines.launch
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
private lateinit var webviewInterface: WebViewInterface
val modId get() = intent.getModId() ?: throw IllegalArgumentException("Invalid Module ID")
val prefs: SharedPreferences get() = getSharedPreferences("settings", MODE_PRIVATE)
val context: Context get() = this
private var rootShell: Shell? = null
override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -37,44 +46,76 @@ class WebUIActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val moduleId = intent.getStringExtra("id")!!
val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name"))
} else {
val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build()
setTaskDescription(taskDescription)
val darkTheme = when (ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> _isSystemInDarkTheme(context)
}
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false))
val colorScheme = createColorScheme(
context = context,
darkTheme = darkTheme
)
val loading = createLoadingRenderer(colorScheme)
setContentView(loading)
lifecycleScope.launch {
val ready = initPlatform(context)
if (ready.await()) {
init()
return@launch
}
confirm(
ConfirmData(
title = "Failed!",
description = "Failed to initialize platform. Please try again.",
confirmText = "Close",
onConfirm = {
finish()
},
),
colorScheme = colorScheme
)
}
}
private fun init() {
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
val options = WebUIOptions(
modId = modId,
debug = webDebugging,
// keep plugins disabled for security reasons
pluginsEnabled = false,
context = context,
)
val moduleDir = "/data/adb/modules/${moduleId}"
val webRoot = File("${moduleDir}/webroot")
val rootShell = createRootShell(true).also { this.rootShell = it }
val webViewAssetLoader = WebViewAssetLoader.Builder()
.setDomain("mui.kernelsu.org")
.addPathHandler(
"/",
SuFilePathHandler(this, webRoot, rootShell)
SuFilePathHandler(this, modId.webrootDir, rootShell)
)
.build()
val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
request: WebResourceRequest,
): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url)
}
}
val webView = WebView(this).apply {
val webView = WebUIView(options).apply {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<MarginLayoutParams> {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = inset.left
rightMargin = inset.right
topMargin = inset.top
@@ -82,20 +123,17 @@ class WebUIActivity : ComponentActivity() {
}
return@setOnApplyWindowInsetsListener insets
}
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
addJavascriptInterface(webviewInterface, "ksu")
addJavascriptInterface<WebViewInterface>()
setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html")
loadDomain()
}
setContentView(webView)
}
override fun onDestroy() {
super.onDestroy()
runCatching { rootShell?.close() }
super.onDestroy()
}
}

View File

@@ -1,110 +1,126 @@
package com.sukisu.ultra.ui.webui
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
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.lifecycle.lifecycleScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.model.ModId
import com.dergoogler.mmrl.ui.component.Loading
import com.dergoogler.mmrl.webui.screen.WebUIScreen
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
import com.dergoogler.mmrl.platform.PlatformManager
import com.dergoogler.mmrl.webui.activity.WXActivity
import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.dergoogler.mmrl.webui.view.WebUIXView
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.ui.theme.KernelSUTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class WebUIXActivity : ComponentActivity() {
private lateinit var webView: WebView
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme._isSystemInDarkTheme
import kotlinx.coroutines.CoroutineScope
import kotlin.jvm.java
class WebUIXActivity : WXActivity() {
private val userAgent
get(): String {
val ksuVersion = BuildConfig.VERSION_CODE
val platform = Platform.get("Unknown") {
platform.name
val platform = PlatformManager.get(Platform.Unknown) {
platform
}
val platformVersion = Platform.get(-1) {
val platformVersion = PlatformManager.get(-1) {
moduleManager.versionCode
}
val osVersion = Build.VERSION.RELEASE
val deviceModel = Build.MODEL
return "SukiSU-Ultra /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
return "SukiSU-Ultra/$ksuVersion (Linux; Android $osVersion; $deviceModel; ${platform.name}/$platformVersion)"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
webView = WebView(this)
val prefs: SharedPreferences get() = getSharedPreferences("settings", MODE_PRIVATE)
val context: Context get() = this
lifecycleScope.launch {
initPlatform()
}
override suspend fun onRender(scope: CoroutineScope) {
super.onRender(scope)
val moduleId = intent.getStringExtra("id")!!
val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name"))
} else {
val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build()
setTaskDescription(taskDescription)
}
val modId =
this.modId
?: throw IllegalArgumentException("modId cannot be null or empty")
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
setContent {
// keep the compose logic so custom background continue to work
KernelSUTheme {
var isLoading by remember { mutableStateOf(true) }
var ready by remember { mutableStateOf(false) }
LaunchedEffect(Platform.isAlive) {
while (!Platform.isAlive) {
delay(1000)
}
isLoading = false
LaunchedEffect(Unit) {
val init = initPlatform(context)
ready = init.await()
}
if (isLoading) {
Loading()
if (!ready) {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
return@KernelSUTheme
}
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
val dark = isSystemInDarkTheme()
val darkTheme = remember(ThemeConfig) {
when (ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> _isSystemInDarkTheme(context)
}
}
val options = rememberWebUIOptions(
modId = ModId(moduleId),
val options = WebUIOptions(
modId = modId,
context = context,
debug = webDebugging,
appVersionCode = BuildConfig.VERSION_CODE,
isDarkMode = dark,
isDarkMode = darkTheme,
// keep plugins disabled for security reasons
pluginsEnabled = false,
enableEruda = erudaInject,
cls = WebUIXActivity::class.java,
userAgentString = userAgent
userAgentString = userAgent,
colorScheme = MaterialTheme.colorScheme
)
WebUIScreen(
webView = webView,
options = options,
interfaces = listOf(
WebViewInterface.factory()
)
// Activity Title
config {
if (title != null) {
setActivityTitle("SukiSU-Ultra - $title")
}
}
AndroidView(
factory = { WebUIXView(options) },
update = { view ->
val v = view.apply {
wx.addJavascriptInterface<WebViewInterface>()
}
// pass it for the activity
this.view = v
}
)
}
}

View File

@@ -1,228 +1,237 @@
package com.sukisu.ultra.ui.webui
import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.dergoogler.mmrl.webui.interfaces.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.sukisu.ultra.ui.util.createRootShell
import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.withNewRootShell
import org.json.JSONArray
import org.json.JSONObject
import com.sukisu.ultra.ui.util.*
import java.io.File
import java.util.concurrent.CompletableFuture
class WebViewInterface(
wxOptions: WXOptions,
) : WXInterface(wxOptions) {
override var name: String = "ksu"
companion object {
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
}
private val modDir get() = "/data/adb/modules/${modId.id}"
@JavascriptInterface
fun exec(cmd: String): String {
return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) }
}
@JavascriptInterface
fun exec(cmd: String, callbackFunc: String) {
exec(cmd, null, callbackFunc)
}
private fun processOptions(sb: StringBuilder, options: String?) {
val opts = if (options == null) JSONObject() else {
JSONObject(options)
}
val cwd = opts.optString("cwd")
if (!TextUtils.isEmpty(cwd)) {
sb.append("cd ${cwd};")
}
opts.optJSONObject("env")?.let { env ->
env.keys().forEach { key ->
sb.append("export ${key}=${env.getString(key)};")
}
}
}
@JavascriptInterface
fun exec(
cmd: String,
options: String?,
callbackFunc: String
) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
finalCommand.append(cmd)
val result = withNewRootShell(true) {
newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec()
}
val stdout = result.out.joinToString(separator = "\n")
val stderr = result.err.joinToString(separator = "\n")
val jsCode =
"javascript: (function() { try { ${callbackFunc}(${result.code}, ${
JSONObject.quote(
stdout
)
}, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
@JavascriptInterface
fun spawn(command: String, args: String, options: String?, callbackFunc: String) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
if (!TextUtils.isEmpty(args)) {
finalCommand.append(command).append(" ")
JSONArray(args).let { argsArray ->
for (i in 0 until argsArray.length()) {
finalCommand.append(argsArray.getString(i))
finalCommand.append(" ")
}
}
} else {
finalCommand.append(command)
}
val shell = createRootShell(true)
val emitData = fun(name: String, data: String) {
val jsCode =
"javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${
JSONObject.quote(
data
)
}); } catch(e) { console.error('emitData', e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
val stdout = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stdout", s)
}
}
val stderr = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stderr", s)
}
}
val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()
val completableFuture = CompletableFuture.supplyAsync {
future.get()
}
completableFuture.thenAccept { result ->
val emitExitCode =
"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
webView.post {
webView.loadUrl(emitExitCode)
}
if (result.code != 0) {
val emitErrCode =
"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${
JSONObject.quote(
result.err.joinToString(
"\n"
)
)
};${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();"
webView.post {
webView.loadUrl(emitErrCode)
}
}
}.whenComplete { _, _ ->
runCatching { shell.close() }
}
}
@JavascriptInterface
fun toast(msg: String) {
webView.post {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun fullScreen(enable: Boolean) {
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(activity.window)
} else {
showSystemUI(activity.window)
}
}
}
}
@JavascriptInterface
fun moduleInfo(): String {
val moduleInfos = JSONArray(listModules())
val currentModuleInfo = JSONObject()
currentModuleInfo.put("moduleDir", modDir)
val moduleId = File(modDir).getName()
for (i in 0 until moduleInfos.length()) {
val currentInfo = moduleInfos.getJSONObject(i)
if (currentInfo.getString("id") != moduleId) {
continue
}
val keys = currentInfo.keys()
for (key in keys) {
currentModuleInfo.put(key, currentInfo.get(key))
}
break
}
return currentModuleInfo.toString()
}
// =================== KPM支持 =============================
@JavascriptInterface
fun listAllKpm() : String {
return listKpmModules()
}
@JavascriptInterface
fun controlKpm(name: String, args: String) : Int {
return controlKpmModule(name, args)
}
}
fun hideSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
package com.sukisu.ultra.ui.webui
import android.app.Activity
import android.content.Context
import android.webkit.WebView
import com.dergoogler.mmrl.platform.model.ModId
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.Log
import android.view.Window
import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.dergoogler.mmrl.platform.file.ExtFile
import com.dergoogler.mmrl.platform.model.ModId.Companion.moduleDir
import com.dergoogler.mmrl.webui.interfaces.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.sukisu.ultra.ui.util.createRootShell
import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.withNewRootShell
import org.json.JSONArray
import org.json.JSONObject
import com.sukisu.ultra.ui.util.*
import java.util.concurrent.CompletableFuture
import kotlin.collections.iterator
internal class WebViewInterface(wxOptions: WXOptions) : WXInterface(wxOptions) {
// `ExtFile` to make sure that the platform won't get called when it is used within KSU WebUI
private val modDir: ExtFile get() = modId.moduleDir.toExtFile()
override var name = "ksu"
@JavascriptInterface
fun exec(cmd: String): String {
return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) }
}
@JavascriptInterface
fun exec(cmd: String, callbackFunc: String) {
exec(cmd, null, callbackFunc)
}
private fun processOptions(sb: StringBuilder, options: String?) {
val opts = if (options == null) JSONObject() else {
JSONObject(options)
}
val cwd = opts.optString("cwd")
if (!TextUtils.isEmpty(cwd)) {
sb.append("cd ${cwd};")
}
opts.optJSONObject("env")?.let { env ->
env.keys().forEach { key ->
sb.append("export ${key}=${env.getString(key)};")
}
}
}
@JavascriptInterface
fun exec(
cmd: String,
options: String?,
callbackFunc: String,
) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
finalCommand.append(cmd)
val result = withNewRootShell(true) {
newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec()
}
val stdout = result.out.joinToString(separator = "\n")
val stderr = result.err.joinToString(separator = "\n")
val jsCode =
"javascript: (function() { try { ${callbackFunc}(${result.code}, ${
JSONObject.quote(
stdout
)
}, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
@JavascriptInterface
fun spawn(command: String, args: String, options: String?, callbackFunc: String) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
if (!TextUtils.isEmpty(args)) {
finalCommand.append(command).append(" ")
JSONArray(args).let { argsArray ->
for (i in 0 until argsArray.length()) {
finalCommand.append(argsArray.getString(i))
finalCommand.append(" ")
}
}
} else {
finalCommand.append(command)
}
val shell = createRootShell(true)
val emitData = fun(name: String, data: String) {
val jsCode =
"javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${
JSONObject.quote(
data
)
}); } catch(e) { console.error('emitData', e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
val stdout = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stdout", s)
}
}
val stderr = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stderr", s)
}
}
val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()
val completableFuture = CompletableFuture.supplyAsync {
future.get()
}
completableFuture.thenAccept { result ->
val emitExitCode =
"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
webView.post {
webView.loadUrl(emitExitCode)
}
if (result.code != 0) {
val emitErrCode =
"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${
JSONObject.quote(
result.err.joinToString(
"\n"
)
)
};${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();"
webView.post {
webView.loadUrl(emitErrCode)
}
}
}.whenComplete { _, _ ->
runCatching { shell.close() }
}
}
@JavascriptInterface
fun toast(msg: String) {
webView.post {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun fullScreen(enable: Boolean) {
if (activity == null) return
try {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(activity!!.window)
} else {
showSystemUI(activity!!.window)
}
}
} catch (e: Exception) {
Log.e("WebViewInterface", "fullScreen", e)
}
}
@JavascriptInterface
fun moduleInfo(): String {
val moduleInfos = JSONArray(listModules())
val currentModuleInfo = JSONObject()
currentModuleInfo.put("moduleDir", modDir.path)
val moduleId = modDir.getName()
for (i in 0 until moduleInfos.length()) {
val currentInfo = moduleInfos.getJSONObject(i)
if (currentInfo.getString("id") != moduleId) {
continue
}
val keys = currentInfo.keys()
for (key in keys) {
currentModuleInfo.put(key, currentInfo.get(key))
}
break
}
return currentModuleInfo.toString()
}
// =================== KPM支持 =============================
@JavascriptInterface
fun listAllKpm(): String {
return listKpmModules()
}
@JavascriptInterface
fun controlKpm(name: String, args: String): Int {
return controlKpmModule(name, args)
}
}
private fun hideSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
private fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(
window,
window.decorView
).show(WindowInsetsCompat.Type.systemBars())