1 Commits
dev ... WX-new

Author SHA1 Message Date
Der_Googler
c5ed6e1e8c 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
2025-08-21 07:21:43 +08:00
13 changed files with 511 additions and 425 deletions

View File

@@ -28,12 +28,12 @@ apksign {
android { android {
/**signingConfigs { /**signingConfigs {
create("Debug") { create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore") storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = "" storePassword = ""
keyAlias = "" keyAlias = ""
keyPassword = "" keyPassword = ""
} }
}**/ }**/
namespace = "com.sukisu.ultra" namespace = "com.sukisu.ultra"
@@ -41,10 +41,13 @@ android {
release { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
/**debug { /**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.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.compose.destinations.core) implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp) ksp(libs.compose.destinations.ksp)
@@ -159,7 +164,16 @@ dependencies {
implementation(libs.mmrl.platform) implementation(libs.mmrl.platform)
compileOnly(libs.mmrl.hidden.api) 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.mmrl.ui)
implementation(libs.accompanist.drawablepainter) implementation(libs.accompanist.drawablepainter)

View File

@@ -4,43 +4,14 @@
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn kotlinx.serialization.** -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 # 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.dergoogler.mmrl.webui.interfaces.** { *; }
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } -keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }

View File

@@ -11,7 +11,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import coil.Coil import coil.Coil
import coil.ImageLoader 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.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File import java.io.File
@@ -90,7 +90,7 @@ class KernelSUApplication : Application() {
// 注册Activity生命周期回调 // 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks) registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
Platform.setHiddenApiExemptions() PlatformManager.setHiddenApiExemptions()
val context = this val context = this
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size) 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 androidx.core.content.edit
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.webui.WebUIXActivity import com.sukisu.ultra.ui.webui.WebUIXActivity
import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri 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
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.component.AnimatedFab import com.sukisu.ultra.ui.component.AnimatedFab
@@ -460,7 +460,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
"wx" -> wxEngine "wx" -> wxEngine
"ksu" -> ksuEngine "ksu" -> ksuEngine
else -> { else -> {
if (Platform.isAlive) { if (PlatformManager.isAlive) {
wxEngine wxEngine
} else { } else {
ksuEngine ksuEngine

View File

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

View File

@@ -7,7 +7,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import android.widget.Toast 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.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell

View File

@@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.Gson
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.KernelVersion

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
package com.sukisu.ultra.ui.webui package com.sukisu.ultra.ui.webui
import android.annotation.SuppressLint import android.content.Context
import android.app.ActivityManager import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
@@ -14,21 +14,30 @@ import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewAssetLoader 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.topjohnwu.superuser.Shell
import com.sukisu.ultra.ui.util.createRootShell import com.sukisu.ultra.ui.util.createRootShell
import java.io.File import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.dergoogler.mmrl.webui.interfaces.WXOptions 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() { 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 private var rootShell: Shell? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge // Enable edge to edge
enableEdgeToEdge() enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -37,44 +46,76 @@ class WebUIActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val moduleId = intent.getStringExtra("id")!! val darkTheme = when (ThemeConfig.forceDarkMode) {
val name = intent.getStringExtra("name")!! true -> true
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { false -> false
@Suppress("DEPRECATION") null -> _isSystemInDarkTheme(context)
setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name"))
} else {
val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build()
setTaskDescription(taskDescription)
} }
val prefs = getSharedPreferences("settings", MODE_PRIVATE) val colorScheme = createColorScheme(
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false)) 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 rootShell = createRootShell(true).also { this.rootShell = it }
val webViewAssetLoader = WebViewAssetLoader.Builder() val webViewAssetLoader = WebViewAssetLoader.Builder()
.setDomain("mui.kernelsu.org") .setDomain("mui.kernelsu.org")
.addPathHandler( .addPathHandler(
"/", "/",
SuFilePathHandler(this, webRoot, rootShell) SuFilePathHandler(this, modId.webrootDir, rootShell)
) )
.build() .build()
val webViewClient = object : WebViewClient() { val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView, view: WebView,
request: WebResourceRequest request: WebResourceRequest,
): WebResourceResponse? { ): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url) return webViewAssetLoader.shouldInterceptRequest(request.url)
} }
} }
val webView = WebView(this).apply { val webView = WebUIView(options).apply {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<MarginLayoutParams> { view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = inset.left leftMargin = inset.left
rightMargin = inset.right rightMargin = inset.right
topMargin = inset.top topMargin = inset.top
@@ -82,20 +123,17 @@ class WebUIActivity : ComponentActivity() {
} }
return@setOnApplyWindowInsetsListener insets return@setOnApplyWindowInsetsListener insets
} }
settings.javaScriptEnabled = true
settings.domStorageEnabled = true addJavascriptInterface<WebViewInterface>()
settings.allowFileAccess = false
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient) setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html") loadDomain()
} }
setContentView(webView) setContentView(webView)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy()
runCatching { rootShell?.close() } runCatching { rootShell?.close() }
super.onDestroy()
} }
} }

View File

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

View File

@@ -1,17 +1,23 @@
package com.sukisu.ultra.ui.webui package com.sukisu.ultra.ui.webui
import android.app.Activity 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.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Window import android.view.Window
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.widget.Toast import android.widget.Toast
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat 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.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.model.JavaScriptInterface import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -21,19 +27,14 @@ import com.sukisu.ultra.ui.util.withNewRootShell
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.*
import java.io.File
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.collections.iterator
class WebViewInterface( internal class WebViewInterface(wxOptions: WXOptions) : WXInterface(wxOptions) {
wxOptions: WXOptions, // `ExtFile` to make sure that the platform won't get called when it is used within KSU WebUI
) : WXInterface(wxOptions) { private val modDir: ExtFile get() = modId.moduleDir.toExtFile()
override var name: String = "ksu"
companion object { override var name = "ksu"
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
}
private val modDir get() = "/data/adb/modules/${modId.id}"
@JavascriptInterface @JavascriptInterface
fun exec(cmd: String): String { fun exec(cmd: String): String {
@@ -66,7 +67,7 @@ class WebViewInterface(
fun exec( fun exec(
cmd: String, cmd: String,
options: String?, options: String?,
callbackFunc: String callbackFunc: String,
) { ) {
val finalCommand = StringBuilder() val finalCommand = StringBuilder()
processOptions(finalCommand, options) processOptions(finalCommand, options)
@@ -172,14 +173,18 @@ class WebViewInterface(
@JavascriptInterface @JavascriptInterface
fun fullScreen(enable: Boolean) { fun fullScreen(enable: Boolean) {
if (context is Activity) { if (activity == null) return
try {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (enable) { if (enable) {
hideSystemUI(activity.window) hideSystemUI(activity!!.window)
} else { } else {
showSystemUI(activity.window) showSystemUI(activity!!.window)
} }
} }
} catch (e: Exception) {
Log.e("WebViewInterface", "fullScreen", e)
} }
} }
@@ -187,8 +192,8 @@ class WebViewInterface(
fun moduleInfo(): String { fun moduleInfo(): String {
val moduleInfos = JSONArray(listModules()) val moduleInfos = JSONArray(listModules())
val currentModuleInfo = JSONObject() val currentModuleInfo = JSONObject()
currentModuleInfo.put("moduleDir", modDir) currentModuleInfo.put("moduleDir", modDir.path)
val moduleId = File(modDir).getName() val moduleId = modDir.getName()
for (i in 0 until moduleInfos.length()) { for (i in 0 until moduleInfos.length()) {
val currentInfo = moduleInfos.getJSONObject(i) val currentInfo = moduleInfos.getJSONObject(i)
@@ -208,21 +213,25 @@ class WebViewInterface(
// =================== KPM支持 ============================= // =================== KPM支持 =============================
@JavascriptInterface @JavascriptInterface
fun listAllKpm() : String { fun listAllKpm(): String {
return listKpmModules() return listKpmModules()
} }
@JavascriptInterface @JavascriptInterface
fun controlKpm(name: String, args: String) : Int { fun controlKpm(name: String, args: String): Int {
return controlKpmModule(name, args) return controlKpmModule(name, args)
} }
} }
fun hideSystemUI(window: Window) = private fun hideSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).let { controller -> WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars()) controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} }
fun showSystemUI(window: Window) = private fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) WindowInsetsControllerCompat(
window,
window.decorView
).show(WindowInsetsCompat.Type.systemBars())

View File

@@ -23,7 +23,9 @@ compose-material = "1.8.3"
compose-material3 = "1.3.2" compose-material3 = "1.3.2"
compose-ui = "1.8.3" compose-ui = "1.8.3"
documentfile = "1.1.0" documentfile = "1.1.0"
mmrl = "2bb00b3c2b" mmrl = "v33953"
webui-x-portable = "953fad192a"
swiperefreshlayout = "1.1.0"
[plugins] [plugins]
agp-app = { id = "com.android.application", version.ref = "agp" } agp-app = { id = "com.android.application", version.ref = "agp" }
@@ -57,6 +59,8 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
@@ -84,8 +88,8 @@ markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown
lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "29.0.13599879-beta2" } lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "29.0.13599879-beta2" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
mmrl-webui-portable = { group = "com.github.MMRLApp.WebUI-X-Portable", name = "webui", version.ref = "webui-x-portable" }
mmrl-webui = { group = "com.github.MMRLApp.MMRL", name = "webui", version.ref = "mmrl" } mmrl-webui-jna = { group = "com.github.MMRLApp.WebUI-X-Portable", name = "jna", version.ref = "webui-x-portable" }
mmrl-platform = { group = "com.github.MMRLApp.MMRL", name = "platform", version.ref = "mmrl" } mmrl-platform = { group = "com.github.MMRLApp.MMRL", name = "platform", version.ref = "mmrl" }
mmrl-ui = { group = "com.github.MMRLApp.MMRL", name = "ui", version.ref = "mmrl" } mmrl-ui = { group = "com.github.MMRLApp.MMRL", name = "ui", version.ref = "mmrl" }
mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" }