Compare commits

...

5 Commits

Author SHA1 Message Date
Der_Googler
d990276a21 Merge branch 'next' into patch-new-wx 2025-05-31 00:24:37 +02:00
Der_Googler
b82f50685d manager: better handle webui engine select 2025-05-30 01:22:30 +02:00
Der_Googler
18219a40b0 manager: update webuix 2025-05-30 01:19:39 +02:00
Der_Googler
1eb6eceb2d manager: opt in to DelicateCoroutinesApi 2025-05-29 19:03:43 +02:00
Der_Googler
c09fecfb1a manager: bump mmrl 2025-05-29 19:03:24 +02:00
9 changed files with 126 additions and 147 deletions

View File

@@ -108,6 +108,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.compose)
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)

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import com.rifsxd.ksunext.ui.webui.initPlatform
import kotlinx.coroutines.DelicateCoroutinesApi
lateinit var ksuApp: KernelSUApplication
@@ -61,6 +62,7 @@ class KernelSUApplication : Application() {
}.build()
}
@OptIn(DelicateCoroutinesApi::class)
private fun launchPlatformInit() {
// Use a coroutine to avoid blocking the main thread
GlobalScope.launch(Dispatchers.IO) {

View File

@@ -71,12 +71,7 @@ class MainActivity : ComponentActivity() {
if (isManager) install()
setContent {
// Read AMOLED mode preference
val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
val amoledMode = prefs.getBoolean("enable_amoled", false)
KernelSUTheme (
amoledMode = amoledMode
) {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }

View File

@@ -311,6 +311,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
.putExtra("id", id)
.putExtra("name", name)
webUILauncher.launch(
if (prefs.getBoolean("use_webuix", true) && Platform.isAlive) {
wxEngine
@@ -318,6 +319,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
ksuEngine
}
)
}
},
context = context,
@@ -660,7 +662,9 @@ fun ModuleItem(
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
LabelItem(
text = if (module.enabled) stringResource(R.string.enabled) else stringResource(R.string.disabled),
text = if (module.enabled) stringResource(R.string.enabled) else stringResource(
R.string.disabled
),
style = if (module.enabled)
com.dergoogler.mmrl.ui.component.LabelItemDefaults.style.copy()
else
@@ -798,7 +802,7 @@ fun ModuleItem(
)
HorizontalDivider()
}
if (module.hasWebUi) {
DropdownMenuItem(
text = { Text(stringResource(R.string.webui)) },
@@ -819,7 +823,7 @@ fun ModuleItem(
)
}
if (module.hasWebUi || module.hasActionScript ) {
if (module.hasWebUi || module.hasActionScript) {
HorizontalDivider()
}

View File

@@ -1,20 +1,19 @@
package com.rifsxd.ksunext.ui.theme
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import com.dergoogler.mmrl.ui.component.StatusBarStyle
private val DarkColorScheme = darkColorScheme(
primary = PRIMARY,
@@ -38,38 +37,58 @@ fun Color.blend(other: Color, ratio: Float): Color {
)
}
@Composable
fun KernelSUTheme(
/**
* AMOLED colors are handled through the context
*/
fun Context.getColorScheme(
darkTheme: Boolean = isSystemInDarkTheme(),
): ColorScheme {
// Read AMOLED mode preference
val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
val amoledMode = prefs.getBoolean("enable_amoled", false)
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
amoledMode: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
val dynamicColor = true
return when {
amoledMode && darkTheme && dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
val dynamicScheme = dynamicDarkColorScheme(context)
val dynamicScheme = dynamicDarkColorScheme(this)
dynamicScheme.copy(
background = AMOLED_BLACK,
surface = AMOLED_BLACK,
surfaceVariant = dynamicScheme.surfaceVariant.blend(AMOLED_BLACK, 0.6f),
surfaceContainer = dynamicScheme.surfaceContainer.blend(AMOLED_BLACK, 0.6f),
surfaceContainerLow = dynamicScheme.surfaceContainerLow.blend(AMOLED_BLACK, 0.6f),
surfaceContainerLowest = dynamicScheme.surfaceContainerLowest.blend(AMOLED_BLACK, 0.6f),
surfaceContainerLowest = dynamicScheme.surfaceContainerLowest.blend(
AMOLED_BLACK,
0.6f
),
surfaceContainerHigh = dynamicScheme.surfaceContainerHigh.blend(AMOLED_BLACK, 0.6f),
surfaceContainerHighest = dynamicScheme.surfaceContainerHighest.blend(AMOLED_BLACK, 0.6f),
surfaceContainerHighest = dynamicScheme.surfaceContainerHighest.blend(
AMOLED_BLACK,
0.6f
),
)
}
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
if (darkTheme) dynamicDarkColorScheme(this) else dynamicLightColorScheme(this)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
}
SystemBarStyle(
@Composable
fun KernelSUTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val colorScheme = context.getColorScheme(darkTheme)
StatusBarStyle(
darkMode = darkTheme
)
@@ -80,31 +99,7 @@ fun KernelSUTheme(
)
}
@Composable
private fun SystemBarStyle(
darkMode: Boolean,
statusBarScrim: Color = Color.Transparent,
navigationBarScrim: Color = Color.Transparent,
) {
val context = LocalContext.current
val activity = context as ComponentActivity
SideEffect {
activity.enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
statusBarScrim.toArgb(),
statusBarScrim.toArgb(),
) { darkMode },
navigationBarStyle = when {
darkMode -> SystemBarStyle.dark(
navigationBarScrim.toArgb()
)
else -> SystemBarStyle.light(
navigationBarScrim.toArgb(),
navigationBarScrim.toArgb(),
)
}
)
}
internal fun isSystemInDarkTheme(): Boolean {
val uiMode = Resources.getSystem().configuration.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}

View File

@@ -17,14 +17,15 @@ import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.dergoogler.mmrl.webui.view.WXView
import com.dergoogler.mmrl.webui.view.WebUIView
import com.topjohnwu.superuser.Shell
import com.rifsxd.ksunext.ui.util.createRootShell
import java.io.File
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
private lateinit var webviewInterface: WebViewInterface
private var rootShell: Shell? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -71,7 +72,12 @@ class WebUIActivity : ComponentActivity() {
}
}
val webView = WebView(this).apply {
val options = WebUIOptions(
modId = ModId(moduleId),
context = this,
)
val webView = WebUIView(options).apply {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<MarginLayoutParams> {
@@ -82,15 +88,15 @@ class WebUIActivity : ComponentActivity() {
}
return@setOnApplyWindowInsetsListener insets
}
val factory = WebViewInterface.factory()
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
webviewInterface = WebViewInterface(
WXOptions(this@WebUIActivity, this, ModId(moduleId))
)
addJavascriptInterface(webviewInterface, "ksu")
addJavascriptInterface(factory)
setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html")
loadDomain()
}
setContentView(webView)

View File

@@ -1,32 +1,19 @@
package com.rifsxd.ksunext.ui.webui
import android.app.ActivityManager
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.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
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.webui.activity.WXActivity
import com.dergoogler.mmrl.webui.util.WebUIOptions
import com.dergoogler.mmrl.webui.view.WebUIXView
import com.rifsxd.ksunext.BuildConfig
import com.rifsxd.ksunext.ui.theme.KernelSUTheme
import kotlinx.coroutines.delay
import com.rifsxd.ksunext.ui.theme.getColorScheme
import com.rifsxd.ksunext.ui.theme.isSystemInDarkTheme
import kotlinx.coroutines.launch
class WebUIXActivity : ComponentActivity() {
private lateinit var webView: WebView
class WebUIXActivity : WXActivity() {
private val userAgent
get(): String {
val ksuVersion = BuildConfig.VERSION_CODE
@@ -45,69 +32,59 @@ class WebUIXActivity : ComponentActivity() {
return "KernelSU Next/$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
override fun onRender(savedInstanceState: Bundle?) {
super.onRender(savedInstanceState)
webView = WebView(this)
lifecycleScope.launch {
initPlatform()
if (this.modId == null) {
val msg = "ModId cannot be null"
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
throw IllegalArgumentException(msg)
}
val moduleId = intent.getStringExtra("id")!!
val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU Next - $name"))
} else {
val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU Next - $name").build()
setTaskDescription(taskDescription)
}
// Cast since we check it
val modId = this.modId!!
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
setContent {
KernelSUTheme {
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Platform.isAlive) {
while (!Platform.isAlive) {
delay(1000)
}
val options = WebUIOptions(
modId = modId,
context = this,
debug = webDebugging,
appVersionCode = BuildConfig.VERSION_CODE,
isDarkMode = isSystemInDarkTheme(),
enableEruda = erudaInject,
cls = WebUIXActivity::class.java,
userAgentString = userAgent,
colorScheme = getColorScheme()
)
isLoading = false
}
val view = WebUIXView(options).apply {
wx.addJavascriptInterface(WebViewInterface.factory())
wx.loadDomain()
}
if (isLoading) {
Loading()
this.options = options
this.view = view
return@KernelSUTheme
}
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
val dark = isSystemInDarkTheme()
// Ensure type safety
val name = intent.getStringExtra("name")
if (name != null) {
setActivityTitle("KernelSU Next - $name")
}
val options = rememberWebUIOptions(
modId = ModId(moduleId),
debug = webDebugging,
appVersionCode = BuildConfig.VERSION_CODE,
isDarkMode = dark,
enableEruda = erudaInject,
cls = WebUIXActivity::class.java,
userAgentString = userAgent
)
val loading = createLoadingRenderer()
setContentView(loading)
WebUIScreen(
webView = webView,
options = options,
interfaces = listOf(
WebViewInterface.factory()
)
)
lifecycleScope.launch {
val deferred = Platform.getAsyncDeferred(this, null) {
view
}
setContentView(deferred.await())
}
}
}

View File

@@ -1,14 +1,12 @@
package com.rifsxd.ksunext.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.platform.model.ModId.Companion.moduleDir
import com.dergoogler.mmrl.webui.interfaces.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
@@ -20,20 +18,19 @@ import com.rifsxd.ksunext.ui.util.listModules
import com.rifsxd.ksunext.ui.util.withNewRootShell
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.CompletableFuture
class WebViewInterface(
wxOptions: WXOptions,
) : WXInterface(wxOptions) {
override var name: String = "ksu"
// Add logging tag for console.log
override var tag: String = "KernelSUInterface"
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) }
@@ -171,23 +168,22 @@ class WebViewInterface(
@JavascriptInterface
fun fullScreen(enable: Boolean) {
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(activity.window)
} else {
showSystemUI(activity.window)
}
runMainLooperPost {
if (enable) {
hideSystemUI(window)
} else {
showSystemUI(window)
}
}
}
@JavascriptInterface
fun moduleInfo(): String {
val modDir = modId.moduleDir
val moduleInfos = JSONArray(listModules())
val currentModuleInfo = JSONObject()
currentModuleInfo.put("moduleDir", modDir)
val moduleId = File(modDir).getName()
val moduleId = modDir.getName()
for (i in 0 until moduleInfos.length()) {
val currentInfo = moduleInfos.getJSONObject(i)

View File

@@ -17,7 +17,8 @@ parcelablelist = "2.0.1"
libsu = "6.0.0"
apksign = "1.4"
cmaker = "1.2"
mmrl = "2bb00b3c2b"
mmrl = "fcb3a1fb76"
swiperefreshlayout = "1.1.0"
[plugins]
agp-app = { id = "com.android.application", version.ref = "agp" }
@@ -36,6 +37,8 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-material = { group = "androidx.compose.material", name = "material" }