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
This commit is contained in:
Der_Googler
2025-04-27 18:01:25 +02:00
committed by GitHub
parent a7a9f86a71
commit babae3b2ad
18 changed files with 466 additions and 163 deletions

View File

@@ -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)
}

View File

@@ -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 { *; }

View File

@@ -33,6 +33,13 @@
android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" />
<activity
android:name=".ui.webui.WebUIXActivity"
android:autoRemoveFromRecents="true"
android:documentLaunchMode="intoExisting"
android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@@ -4,6 +4,7 @@ import android.app.Application
import android.system.Os
import coil.Coil
import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import okhttp3.Cache
@@ -21,6 +22,8 @@ class KernelSUApplication : Application() {
super.onCreate()
ksuApp = this
Platform.setHiddenApiExemptions()
val context = this
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
Coil.setImageLoader(

View File

@@ -1,77 +0,0 @@
package com.rifsxd.ksunext.ui;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.ipc.RootService;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.rifsxd.ksunext.IKsuInterface;
import rikka.parcelablelist.ParcelableListSlice;
/**
* @author weishu
* @date 2023/4/18.
*/
public class KsuService extends RootService {
private static final String TAG = "KsuService";
class Stub extends IKsuInterface.Stub {
@Override
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
List<PackageInfo> list = getInstalledPackagesAll(flags);
Log.i(TAG, "getPackages: " + list.size());
return new ParcelableListSlice<>(list);
}
}
@Override
public IBinder onBind(@NonNull Intent intent) {
return new Stub();
}
List<Integer> getUserIds() {
List<Integer> result = new ArrayList<>();
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
List<UserHandle> userProfiles = um.getUserProfiles();
for (UserHandle userProfile : userProfiles) {
int userId = userProfile.hashCode();
result.add(userProfile.hashCode());
}
return result;
}
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
ArrayList<PackageInfo> packages = new ArrayList<>();
for (Integer userId : getUserIds()) {
Log.i(TAG, "getInstalledPackagesAll: " + userId);
packages.addAll(getInstalledPackagesAsUser(flags, userId));
}
return packages;
}
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
try {
PackageManager pm = getPackageManager();
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
} catch (Throwable e) {
Log.e(TAG, "err", e);
}
return new ArrayList<>();
}
}

View File

@@ -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<PackageInfo> {
val list: List<PackageInfo> = getInstalledPackagesAll(flags)
Log.i(TAG, "getPackages: " + list.size)
return ParcelableListSlice(list)
}
private val userIds: List<Int>
get() {
val result: MutableList<Int> =
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<PackageInfo> {
val packages = ArrayList<PackageInfo>()
for (userId in userIds) {
Log.i(TAG, "getInstalledPackagesAll: $userId")
packages.addAll(getInstalledPackagesAsUser(flags, userId))
}
return packages
}
private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {
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<PackageInfo>
} catch (e: Throwable) {
Log.e(TAG, "err", e)
}
return ArrayList()
}
private companion object Default {
const val TAG = "KsuService"
}
}

View File

@@ -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(

View File

@@ -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<RootGraph>
@@ -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<ModuleViewModel>()
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)
)
}
}
}
}
}

View File

@@ -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)) },

View File

@@ -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(),
)
}
)
}
}

View File

@@ -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<IBinder, ServiceConnection> = 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

View File

@@ -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<PackageInfo> {
val ksuService: IKsuInterface by lazy {
IKsuInterface.Stub.asInterface(
this.mService.getService("ksuService")
)
}
return ksuService.getPackages(flags)
}

View File

@@ -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)
}
}

View File

@@ -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")

View File

@@ -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()
)
)
}
}
}
}

View File

@@ -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())

View File

@@ -194,4 +194,6 @@
<string name="settings_disable_su">Disable su compatibility</string>
<string name="settings_disable_su_summary">Temporarily disable the ability of any app to gain root privileges via the su command (existing root processes won\'t be affected).</string>
<string name="settings_language">Language</string>
<string name="use_webuix">Use WebUI X</string>
<string name="use_webuix_summary">Use WebUI X instead of WebUI which supports more API\'s</string>
</resources>

View File

@@ -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" }
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" }