From babae3b2ad29c85a231efa2f495d5c720fd6c079 Mon Sep 17 00:00:00 2001
From: Der_Googler <54764558+DerGoogler@users.noreply.github.com>
Date: Sun, 27 Apr 2025 18:01:25 +0200
Subject: [PATCH] Add WebUI X (#316)
* Add option to use WebUI X
- Added WebUI X from MMRL to KernelSU Next
* Some improvements
* Again some improvements
* Fix NPE
* Still not fixed
Use mappings from the action to decode the stacktrace
* bump mmrl
does still not work
* Works
* WORKS
* small fixes
---
manager/app/build.gradle.kts | 5 +
manager/app/proguard-rules.pro | 47 ++++++++++
manager/app/src/main/AndroidManifest.xml | 7 ++
.../com/rifsxd/ksunext/KernelSUApplication.kt | 3 +
.../com/rifsxd/ksunext/ui/KsuService.java | 77 ----------------
.../java/com/rifsxd/ksunext/ui/KsuService.kt | 84 +++++++++++++++++
.../com/rifsxd/ksunext/ui/MainActivity.kt | 13 ++-
.../com/rifsxd/ksunext/ui/screen/Module.kt | 42 ++++++---
.../com/rifsxd/ksunext/ui/screen/Settings.kt | 24 ++++-
.../java/com/rifsxd/ksunext/ui/theme/Theme.kt | 39 ++++++++
.../ui/viewmodel/SuperUserViewModel.kt | 54 +----------
.../ksunext/ui/webui/KsuLibSuProvider.kt | 75 +++++++++++++++
.../com/rifsxd/ksunext/ui/webui/SuService.kt | 14 +++
.../rifsxd/ksunext/ui/webui/WebUIActivity.kt | 11 ++-
.../rifsxd/ksunext/ui/webui/WebUIXActivity.kt | 92 +++++++++++++++++++
.../ksunext/ui/webui/WebViewInterface.kt | 32 ++++---
manager/app/src/main/res/values/strings.xml | 2 +
manager/gradle/libs.versions.toml | 8 +-
18 files changed, 466 insertions(+), 163 deletions(-)
delete mode 100644 manager/app/src/main/java/com/rifsxd/ksunext/ui/KsuService.java
create mode 100644 manager/app/src/main/java/com/rifsxd/ksunext/ui/KsuService.kt
create mode 100644 manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/KsuLibSuProvider.kt
create mode 100644 manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/SuService.kt
create mode 100644 manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIXActivity.kt
diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts
index 01f7884a..df821b47 100644
--- a/manager/app/build.gradle.kts
+++ b/manager/app/build.gradle.kts
@@ -133,4 +133,9 @@ dependencies {
implementation(libs.androidx.webkit)
implementation(libs.lsposed.cxx)
+
+ implementation(libs.mmrl.platform)
+ compileOnly(libs.mmrl.hidden.api)
+ implementation(libs.mmrl.ui)
+ implementation(libs.mmrl.webui)
}
\ No newline at end of file
diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro
index e69de29b..5420195e 100644
--- a/manager/app/proguard-rules.pro
+++ b/manager/app/proguard-rules.pro
@@ -0,0 +1,47 @@
+-verbose
+-optimizationpasses 5
+
+-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 com.dergoogler.mmrl.webui.interfaces.** { *; }
+-keep class com.rifsxd.ksunext.ui.webui.WebViewInterface { *; }
+
+-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
\ No newline at end of file
diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml
index 11cda5f2..c3ec0dea 100644
--- a/manager/app/src/main/AndroidManifest.xml
+++ b/manager/app/src/main/AndroidManifest.xml
@@ -33,6 +33,13 @@
android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" />
+
+
getPackages(int flags) {
- List list = getInstalledPackagesAll(flags);
- Log.i(TAG, "getPackages: " + list.size());
- return new ParcelableListSlice<>(list);
- }
- }
-
- @Override
- public IBinder onBind(@NonNull Intent intent) {
- return new Stub();
- }
-
- List getUserIds() {
- List result = new ArrayList<>();
- UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
- List userProfiles = um.getUserProfiles();
- for (UserHandle userProfile : userProfiles) {
- int userId = userProfile.hashCode();
- result.add(userProfile.hashCode());
- }
- return result;
- }
-
- ArrayList getInstalledPackagesAll(int flags) {
- ArrayList packages = new ArrayList<>();
- for (Integer userId : getUserIds()) {
- Log.i(TAG, "getInstalledPackagesAll: " + userId);
- packages.addAll(getInstalledPackagesAsUser(flags, userId));
- }
- return packages;
- }
-
- List getInstalledPackagesAsUser(int flags, int userId) {
- try {
- PackageManager pm = getPackageManager();
- Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
- return (List) getInstalledPackagesAsUser.invoke(pm, flags, userId);
- } catch (Throwable e) {
- Log.e(TAG, "err", e);
- }
-
- return new ArrayList<>();
- }
-}
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/KsuService.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/KsuService.kt
new file mode 100644
index 00000000..38af3008
--- /dev/null
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/KsuService.kt
@@ -0,0 +1,84 @@
+package com.rifsxd.ksunext.ui
+
+import android.app.ActivityThread
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.PackageInfo
+import android.os.IBinder
+import android.os.UserManager
+import android.util.Log
+import com.dergoogler.mmrl.platform.content.IService
+import com.dergoogler.mmrl.platform.stub.IServiceManager
+import com.rifsxd.ksunext.IKsuInterface
+import com.topjohnwu.superuser.ipc.RootService.USER_SERVICE
+import rikka.parcelablelist.ParcelableListSlice
+
+class KsuService: IService {
+ override val name: String
+ get() = "ksuService"
+
+ override fun create(manager: IServiceManager): IBinder = KsuServiceImpl()
+}
+
+class KsuServiceImpl : IKsuInterface.Stub() {
+ val context: Context
+ get() {
+ var context: Context = ActivityThread.currentApplication()
+ while (context is ContextWrapper) {
+ context = context.baseContext
+ }
+
+ return context
+ }
+
+
+ override fun getPackages(flags: Int): ParcelableListSlice {
+ val list: List = getInstalledPackagesAll(flags)
+ Log.i(TAG, "getPackages: " + list.size)
+ return ParcelableListSlice(list)
+ }
+
+ private val userIds: List
+ get() {
+ val result: MutableList =
+ ArrayList()
+ val um =
+ context.getSystemService(USER_SERVICE) as UserManager
+ val userProfiles = um.userProfiles
+ for (userProfile in userProfiles) {
+ val userId = userProfile.hashCode()
+ result.add(userProfile.hashCode())
+ }
+ return result
+ }
+
+ private fun getInstalledPackagesAll(flags: Int): ArrayList {
+ val packages = ArrayList()
+ for (userId in userIds) {
+ Log.i(TAG, "getInstalledPackagesAll: $userId")
+ packages.addAll(getInstalledPackagesAsUser(flags, userId))
+ }
+ return packages
+ }
+
+ private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List {
+ try {
+ val pm = context.packageManager
+ val getInstalledPackagesAsUser = pm.javaClass.getDeclaredMethod(
+ "getInstalledPackagesAsUser",
+ Int::class.javaPrimitiveType,
+ Int::class.javaPrimitiveType
+ )
+
+ return getInstalledPackagesAsUser.invoke(pm, flags, userId) as List
+ } catch (e: Throwable) {
+ Log.e(TAG, "err", e)
+ }
+
+ return ArrayList()
+ }
+
+ private companion object Default {
+ const val TAG = "KsuService"
+ }
+}
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/MainActivity.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/MainActivity.kt
index 7e4847ca..e127a7ab 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/MainActivity.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/MainActivity.kt
@@ -29,6 +29,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -38,6 +39,7 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.currentBackStackEntryAsState
+import com.dergoogler.mmrl.platform.Platform
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
@@ -52,7 +54,7 @@ import com.rifsxd.ksunext.ui.theme.KernelSUTheme
import com.rifsxd.ksunext.ui.util.LocalSnackbarHost
import com.rifsxd.ksunext.ui.util.rootAvailable
import com.rifsxd.ksunext.ui.util.install
-import com.rifsxd.ksunext.ui.util.*
+import com.rifsxd.ksunext.ui.webui.initPlatform
class MainActivity : ComponentActivity() {
@@ -67,7 +69,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val isManager = Natives.becomeManager(ksuApp.packageName)
- if (isManager) install()
+ if (isManager) install()
setContent {
KernelSUTheme {
@@ -81,6 +83,11 @@ class MainActivity : ComponentActivity() {
else -> true
}
+ // pre-init platform to faster start WebUI X activities
+ LaunchedEffect(Unit) {
+ initPlatform()
+ }
+
Scaffold(
bottomBar = {
AnimatedVisibility(
@@ -118,7 +125,7 @@ class MainActivity : ComponentActivity() {
private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName)
- val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
+ val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() && Platform.isAlive
NavigationBar(
tonalElevation = 8.dp,
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt
index 6aedac43..849aad41 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Module.kt
@@ -108,6 +108,7 @@ import com.rifsxd.ksunext.ui.util.uninstallModule
import com.rifsxd.ksunext.ui.util.restoreModule
import com.rifsxd.ksunext.ui.viewmodel.ModuleViewModel
import com.rifsxd.ksunext.ui.webui.WebUIActivity
+import com.rifsxd.ksunext.ui.webui.WebUIXActivity
@OptIn(ExperimentalMaterial3Api::class)
@Destination
@@ -238,15 +239,18 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
}
// Show confirm dialog with selected zip file(s) name(s)
- val moduleNames = uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("")
- val confirmContent = context.getString(R.string.module_install_prompt_with_name, moduleNames)
+ val moduleNames =
+ uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }
+ .joinToString("")
+ val confirmContent =
+ context.getString(R.string.module_install_prompt_with_name, moduleNames)
zipUris = uris
confirmDialog.showConfirm(
title = confirmTitle,
content = confirmContent,
markdown = true
)
-
+
}
ExtendedFloatingActionButton(
@@ -281,6 +285,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
)
}
}
+
else -> {
ModuleList(
navigator,
@@ -293,10 +298,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi ->
if (hasWebUi) {
webUILauncher.launch(
- Intent(context, WebUIActivity::class.java)
- .setData(Uri.parse("kernelsu://webui/$id"))
- .putExtra("id", id)
- .putExtra("name", name)
+ if (prefs.getBoolean("use_webuix", false)) {
+ Intent(context, WebUIXActivity::class.java)
+ .setData(Uri.parse("kernelsu://webuix/$id"))
+ .putExtra("id", id)
+ .putExtra("name", name)
+ } else {
+ Intent(context, WebUIActivity::class.java)
+ .setData(Uri.parse("kernelsu://webui/$id"))
+ .putExtra("id", id)
+ .putExtra("name", name)
+ }
)
}
},
@@ -318,7 +330,7 @@ private fun ModuleList(
onInstallModule: (Uri) -> Unit,
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit,
context: Context,
- snackBarHost: SnackbarHostState
+ snackBarHost: SnackbarHostState,
) {
val failedEnable = stringResource(R.string.module_failed_to_enable)
val failedDisable = stringResource(R.string.module_failed_to_disable)
@@ -342,7 +354,8 @@ private fun ModuleList(
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
- val hasShownWarning = rememberSaveable { mutableStateOf(prefs.getBoolean("has_shown_warning", false)) }
+ val hasShownWarning =
+ rememberSaveable { mutableStateOf(prefs.getBoolean("has_shown_warning", false)) }
var useOverlayFs by rememberSaveable {
mutableStateOf(
@@ -357,7 +370,7 @@ private fun ModuleList(
module: ModuleViewModel.ModuleInfo,
changelogUrl: String,
downloadUrl: String,
- fileName: String
+ fileName: String,
) {
val changelogResult = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
@@ -570,7 +583,8 @@ private fun ModuleList(
reboot()
}
} else {
- val message = if (module.enabled) failedDisable else failedEnable
+ val message =
+ if (module.enabled) failedDisable else failedEnable
snackBarHost.showSnackbar(message.format(module.name))
}
}
@@ -611,7 +625,7 @@ fun ModuleItem(
onRestore: (ModuleViewModel.ModuleInfo) -> Unit,
onCheckChanged: (Boolean) -> Unit,
onUpdate: (ModuleViewModel.ModuleInfo) -> Unit,
- onClick: (ModuleViewModel.ModuleInfo) -> Unit
+ onClick: (ModuleViewModel.ModuleInfo) -> Unit,
) {
ElevatedCard(
modifier = Modifier.fillMaxWidth()
@@ -620,7 +634,7 @@ fun ModuleItem(
val interactionSource = remember { MutableInteractionSource() }
val indication = LocalIndication.current
val viewModel = viewModel()
-
+
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -862,7 +876,7 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.uninstall)
)
- }
+ }
}
}
}
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt
index 381f5d06..c1ee232f 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/screen/Settings.kt
@@ -85,6 +85,7 @@ import com.rifsxd.ksunext.ui.util.getBugreportFile
import com.rifsxd.ksunext.ui.util.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
+import androidx.core.content.edit
/**
* @author weishu
@@ -162,7 +163,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked
-
+
) {
if (Natives.setDefaultUmountModules(it)) {
umountChecked = it
@@ -319,6 +320,23 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
}
+ var useWebUIX by rememberSaveable {
+ mutableStateOf(
+ prefs.getBoolean("use_webuix", false)
+ )
+ }
+ if (ksuVersion != null) {
+ SwitchItem(
+ icon = Icons.Filled.WebAsset,
+ title = stringResource(id = R.string.use_webuix),
+ summary = stringResource(id = R.string.use_webuix_summary),
+ checked = useWebUIX
+ ) {
+ prefs.edit().putBoolean("use_webuix", it).apply()
+ useWebUIX = it
+ }
+ }
+
if (ksuVersion != null) {
val backupRestore = stringResource(id = R.string.backup_restore)
ListItem(
@@ -496,7 +514,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
@Composable
fun UninstallItem(
navigator: DestinationsNavigator,
- withLoading: suspend (suspend () -> Unit) -> Unit
+ withLoading: suspend (suspend () -> Unit) -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -598,7 +616,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
- scrollBehavior: TopAppBarScrollBehavior? = null
+ scrollBehavior: TopAppBarScrollBehavior? = null,
) {
TopAppBar(
title = { Text(stringResource(R.string.settings)) },
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/theme/Theme.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/theme/Theme.kt
index 896aa0eb..751708d8 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/theme/Theme.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/theme/Theme.kt
@@ -1,6 +1,9 @@
package com.rifsxd.ksunext.ui.theme
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.MaterialTheme
import androidx.compose.material3.darkColorScheme
@@ -8,6 +11,9 @@ 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
private val DarkColorScheme = darkColorScheme(
@@ -38,9 +44,42 @@ fun KernelSUTheme(
else -> LightColorScheme
}
+ SystemBarStyle(
+ darkMode = darkTheme
+ )
+
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
+
+@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(),
+ )
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/viewmodel/SuperUserViewModel.kt
index f2ce58ce..2b38d172 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/viewmodel/SuperUserViewModel.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/viewmodel/SuperUserViewModel.kt
@@ -1,11 +1,7 @@
package com.rifsxd.ksunext.ui.viewmodel
-import android.content.ComponentName
-import android.content.Intent
-import android.content.ServiceConnection
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
-import android.os.IBinder
import android.os.Parcelable
import android.os.SystemClock
import android.util.Log
@@ -14,20 +10,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
-import com.topjohnwu.superuser.Shell
+import com.dergoogler.mmrl.platform.Platform
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
-import com.rifsxd.ksunext.IKsuInterface
import com.rifsxd.ksunext.Natives
import com.rifsxd.ksunext.ksuApp
-import com.rifsxd.ksunext.ui.KsuService
import com.rifsxd.ksunext.ui.util.HanziToPinyin
-import com.rifsxd.ksunext.ui.util.KsuCli
+import com.rifsxd.ksunext.ui.webui.getPackages
import java.text.Collator
import java.util.*
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
class SuperUserViewModel : ViewModel() {
@@ -97,55 +89,15 @@ class SuperUserViewModel : ViewModel() {
}
}
- private suspend inline fun connectKsuService(
- crossinline onDisconnect: () -> Unit = {}
- ): Pair = suspendCoroutine {
- val connection = object : ServiceConnection {
- override fun onServiceDisconnected(name: ComponentName?) {
- onDisconnect()
- }
-
- override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
- it.resume(binder as IBinder to this)
- }
- }
-
- val intent = Intent(ksuApp, KsuService::class.java)
-
- val task = KsuService.bindOrTask(
- intent,
- Shell.EXECUTOR,
- connection,
- )
- val shell = KsuCli.SHELL
- task?.let { it1 -> shell.execTask(it1) }
- }
-
- private fun stopKsuService() {
- val intent = Intent(ksuApp, KsuService::class.java)
- KsuService.stop(intent)
- }
suspend fun fetchAppList() {
-
isRefreshing = true
- val result = connectKsuService {
- Log.w(TAG, "KsuService disconnected")
- }
-
withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime()
- val binder = result.first
- val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
-
- withContext(Dispatchers.Main) {
- stopKsuService()
- }
-
- val packages = allPackages.list
+ val packages = Platform.getPackages(0).list
apps = packages.map {
val appInfo = it.applicationInfo
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/KsuLibSuProvider.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/KsuLibSuProvider.kt
new file mode 100644
index 00000000..e534751a
--- /dev/null
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/KsuLibSuProvider.kt
@@ -0,0 +1,75 @@
+package com.rifsxd.ksunext.ui.webui
+
+import android.content.Context
+import android.content.ServiceConnection
+import android.content.pm.PackageInfo
+import android.util.Log
+import com.dergoogler.mmrl.platform.Platform
+import com.dergoogler.mmrl.platform.content.Service
+import com.dergoogler.mmrl.platform.model.IProvider
+import com.dergoogler.mmrl.platform.model.PlatformIntent
+import com.rifsxd.ksunext.IKsuInterface
+import com.rifsxd.ksunext.Natives
+import com.rifsxd.ksunext.ksuApp
+import com.rifsxd.ksunext.ui.KsuService
+import com.topjohnwu.superuser.ipc.RootService
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import rikka.parcelablelist.ParcelableListSlice
+
+class KsuLibSuProvider : IProvider {
+ override val name = "KsuLibSu"
+
+ override fun isAvailable() = true
+
+ override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
+
+ private val serviceIntent
+ get() = PlatformIntent(
+ ksuApp,
+ Platform.KsuNext,
+ SuService::class.java
+ )
+
+ override fun bind(connection: ServiceConnection) {
+ RootService.bind(serviceIntent.intent, connection)
+ }
+
+ override fun unbind(connection: ServiceConnection) {
+ RootService.stop(serviceIntent.intent)
+ }
+}
+
+suspend fun initPlatform() = withContext(Dispatchers.IO) {
+ try {
+ val active = Platform.init {
+ this.context = ksuApp
+ this.platform = Platform.KsuNext
+ this.provider = from(KsuLibSuProvider())
+ }
+
+ while (!active) {
+ delay(1000)
+ }
+
+ Platform.mService.addService(
+ Service(KsuService::class.java)
+ )
+
+ return@withContext active
+ } catch (e: Exception) {
+ Log.e("KsuLibSu", "Failed to initialize platform", e)
+ return@withContext false
+ }
+}
+
+fun Platform.Companion.getPackages(flags: Int): ParcelableListSlice {
+ val ksuService: IKsuInterface by lazy {
+ IKsuInterface.Stub.asInterface(
+ this.mService.getService("ksuService")
+ )
+ }
+
+ return ksuService.getPackages(flags)
+}
\ No newline at end of file
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/SuService.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/SuService.kt
new file mode 100644
index 00000000..ab1c3b7a
--- /dev/null
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/SuService.kt
@@ -0,0 +1,14 @@
+package com.rifsxd.ksunext.ui.webui
+
+import android.content.Intent
+import android.os.IBinder
+import com.dergoogler.mmrl.platform.model.PlatformIntent.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()
+ return ServiceManager(mode)
+ }
+}
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIActivity.kt
index 372e8fde..f582c2c5 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIActivity.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIActivity.kt
@@ -15,6 +15,8 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader
+import com.dergoogler.mmrl.webui.interfaces.WXOptions
+import com.dergoogler.mmrl.webui.model.ModId
import com.topjohnwu.superuser.Shell
import com.rifsxd.ksunext.ui.util.createRootShell
import java.io.File
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
} else {
- val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
+ val taskDescription =
+ ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
setTaskDescription(taskDescription)
}
@@ -62,7 +65,7 @@ class WebUIActivity : ComponentActivity() {
val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
- request: WebResourceRequest
+ request: WebResourceRequest,
): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url)
}
@@ -82,7 +85,9 @@ class WebUIActivity : ComponentActivity() {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
- webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
+ webviewInterface = WebViewInterface(
+ WXOptions(this@WebUIActivity, this, ModId(moduleId))
+ )
addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html")
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIXActivity.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIXActivity.kt
new file mode 100644
index 00000000..24b56cfb
--- /dev/null
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebUIXActivity.kt
@@ -0,0 +1,92 @@
+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 androidx.lifecycle.lifecycleScope
+import com.dergoogler.mmrl.platform.Platform
+import com.dergoogler.mmrl.ui.component.Loading
+import com.dergoogler.mmrl.webui.interfaces.WXOptions
+import com.dergoogler.mmrl.webui.model.ModId
+import com.dergoogler.mmrl.webui.screen.WebUIScreen
+import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
+import com.rifsxd.ksunext.BuildConfig
+import com.rifsxd.ksunext.ui.theme.KernelSUTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class WebUIXActivity : ComponentActivity() {
+ private lateinit var webView: WebView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ webView = WebView(this)
+
+ lifecycleScope.launch {
+ initPlatform()
+ }
+
+ 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 - $name"))
+ } else {
+ val taskDescription =
+ ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
+ setTaskDescription(taskDescription)
+ }
+
+ val prefs = getSharedPreferences("settings", MODE_PRIVATE)
+
+ setContent {
+ KernelSUTheme {
+ var isLoading by remember { mutableStateOf(true) }
+
+ LaunchedEffect(Platform.isAlive) {
+ while (!Platform.isAlive) {
+ delay(1000)
+ }
+
+ isLoading = false
+ }
+
+ if (isLoading) {
+ Loading()
+
+ return@KernelSUTheme
+ }
+
+ val webDebugging = prefs.getBoolean("enable_web_debugging", false)
+ val dark = isSystemInDarkTheme()
+
+ val options = rememberWebUIOptions(
+ modId = ModId(moduleId),
+ debug = webDebugging,
+ appVersionCode = BuildConfig.VERSION_CODE,
+ isDarkMode = dark,
+ )
+
+ WebUIScreen(
+ webView = webView,
+ options = options,
+ interfaces = listOf(
+ WebViewInterface.factory()
+ )
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebViewInterface.kt
index 649c71c6..a33c1e09 100644
--- a/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebViewInterface.kt
+++ b/manager/app/src/main/java/com/rifsxd/ksunext/ui/webui/WebViewInterface.kt
@@ -1,16 +1,17 @@
package com.rifsxd.ksunext.ui.webui
import android.app.Activity
-import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
-import android.webkit.WebView
import android.widget.Toast
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
+import com.dergoogler.mmrl.webui.interfaces.WXOptions
+import com.dergoogler.mmrl.webui.interfaces.WebUIInterface
+import com.dergoogler.mmrl.webui.model.JavaScriptInterface
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -23,10 +24,15 @@ import java.io.File
import java.util.concurrent.CompletableFuture
class WebViewInterface(
- val context: Context,
- private val webView: WebView,
- private val modDir: String
-) {
+ wxOptions: WXOptions,
+) : WebUIInterface(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 {
@@ -59,7 +65,7 @@ class WebViewInterface(
fun exec(
cmd: String,
options: String?,
- callbackFunc: String
+ callbackFunc: String,
) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
@@ -168,9 +174,9 @@ class WebViewInterface(
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
- hideSystemUI(context.window)
+ hideSystemUI(activity.window)
} else {
- showSystemUI(context.window)
+ showSystemUI(activity.window)
}
}
}
@@ -202,8 +208,12 @@ class WebViewInterface(
fun hideSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
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) =
- WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
+ WindowInsetsControllerCompat(
+ window,
+ window.decorView
+ ).show(WindowInsetsCompat.Type.systemBars())
diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml
index b34d6299..b9f26970 100644
--- a/manager/app/src/main/res/values/strings.xml
+++ b/manager/app/src/main/res/values/strings.xml
@@ -194,4 +194,6 @@
Disable su compatibility
Temporarily disable the ability of any app to gain root privileges via the su command (existing root processes won\'t be affected).
Language
+ Use WebUI X
+ Use WebUI X instead of WebUI which supports more API\'s
diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml
index b35619c8..6c603d20 100644
--- a/manager/gradle/libs.versions.toml
+++ b/manager/gradle/libs.versions.toml
@@ -17,6 +17,7 @@ parcelablelist = "2.0.1"
libsu = "6.0.0"
apksign = "1.4"
cmaker = "1.2"
+mmrl = "785605169b"
[plugins]
agp-app = { id = "com.android.application", version.ref = "agp" }
@@ -71,4 +72,9 @@ sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs"
markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" }
-lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
\ No newline at end of file
+lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
+
+mmrl-webui = { group = "com.github.MMRLApp.MMRL", name = "webui", 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-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" }
\ No newline at end of file