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
+5
View File
@@ -133,4 +133,9 @@ dependencies {
implementation(libs.androidx.webkit) implementation(libs.androidx.webkit)
implementation(libs.lsposed.cxx) implementation(libs.lsposed.cxx)
implementation(libs.mmrl.platform)
compileOnly(libs.mmrl.hidden.api)
implementation(libs.mmrl.ui)
implementation(libs.mmrl.webui)
} }
+47
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 { *; }
+7
View File
@@ -33,6 +33,13 @@
android:exported="false" android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" /> 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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
@@ -4,6 +4,7 @@ import android.app.Application
import android.system.Os import android.system.Os
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform
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 okhttp3.Cache import okhttp3.Cache
@@ -21,6 +22,8 @@ class KernelSUApplication : Application() {
super.onCreate() super.onCreate()
ksuApp = this ksuApp = this
Platform.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)
Coil.setImageLoader( Coil.setImageLoader(
@@ -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<>();
}
}
@@ -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"
}
}
@@ -29,6 +29,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -38,6 +39,7 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import com.dergoogler.mmrl.platform.Platform
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination 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.LocalSnackbarHost
import com.rifsxd.ksunext.ui.util.rootAvailable import com.rifsxd.ksunext.ui.util.rootAvailable
import com.rifsxd.ksunext.ui.util.install import com.rifsxd.ksunext.ui.util.install
import com.rifsxd.ksunext.ui.util.* import com.rifsxd.ksunext.ui.webui.initPlatform
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -67,7 +69,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = Natives.becomeManager(ksuApp.packageName)
if (isManager) install() if (isManager) install()
setContent { setContent {
KernelSUTheme { KernelSUTheme {
@@ -81,6 +83,11 @@ class MainActivity : ComponentActivity() {
else -> true else -> true
} }
// pre-init platform to faster start WebUI X activities
LaunchedEffect(Unit) {
initPlatform()
}
Scaffold( Scaffold(
bottomBar = { bottomBar = {
AnimatedVisibility( AnimatedVisibility(
@@ -118,7 +125,7 @@ class MainActivity : ComponentActivity() {
private fun BottomBar(navController: NavHostController) { private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator() val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() && Platform.isAlive
NavigationBar( NavigationBar(
tonalElevation = 8.dp, tonalElevation = 8.dp,
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
@@ -108,6 +108,7 @@ import com.rifsxd.ksunext.ui.util.uninstallModule
import com.rifsxd.ksunext.ui.util.restoreModule import com.rifsxd.ksunext.ui.util.restoreModule
import com.rifsxd.ksunext.ui.viewmodel.ModuleViewModel import com.rifsxd.ksunext.ui.viewmodel.ModuleViewModel
import com.rifsxd.ksunext.ui.webui.WebUIActivity import com.rifsxd.ksunext.ui.webui.WebUIActivity
import com.rifsxd.ksunext.ui.webui.WebUIXActivity
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@@ -238,15 +239,18 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} }
// Show confirm dialog with selected zip file(s) name(s) // Show confirm dialog with selected zip file(s) name(s)
val moduleNames = uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("") val moduleNames =
val confirmContent = context.getString(R.string.module_install_prompt_with_name, 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 zipUris = uris
confirmDialog.showConfirm( confirmDialog.showConfirm(
title = confirmTitle, title = confirmTitle,
content = confirmContent, content = confirmContent,
markdown = true markdown = true
) )
} }
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
@@ -281,6 +285,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
} }
} }
else -> { else -> {
ModuleList( ModuleList(
navigator, navigator,
@@ -293,10 +298,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi -> onClickModule = { id, name, hasWebUi ->
if (hasWebUi) { if (hasWebUi) {
webUILauncher.launch( webUILauncher.launch(
Intent(context, WebUIActivity::class.java) if (prefs.getBoolean("use_webuix", false)) {
.setData(Uri.parse("kernelsu://webui/$id")) Intent(context, WebUIXActivity::class.java)
.putExtra("id", id) .setData(Uri.parse("kernelsu://webuix/$id"))
.putExtra("name", name) .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, onInstallModule: (Uri) -> Unit,
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit,
context: Context, context: Context,
snackBarHost: SnackbarHostState snackBarHost: SnackbarHostState,
) { ) {
val failedEnable = stringResource(R.string.module_failed_to_enable) val failedEnable = stringResource(R.string.module_failed_to_enable)
val failedDisable = stringResource(R.string.module_failed_to_disable) 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 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 { var useOverlayFs by rememberSaveable {
mutableStateOf( mutableStateOf(
@@ -357,7 +370,7 @@ private fun ModuleList(
module: ModuleViewModel.ModuleInfo, module: ModuleViewModel.ModuleInfo,
changelogUrl: String, changelogUrl: String,
downloadUrl: String, downloadUrl: String,
fileName: String fileName: String,
) { ) {
val changelogResult = loadingDialog.withLoading { val changelogResult = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -570,7 +583,8 @@ private fun ModuleList(
reboot() reboot()
} }
} else { } else {
val message = if (module.enabled) failedDisable else failedEnable val message =
if (module.enabled) failedDisable else failedEnable
snackBarHost.showSnackbar(message.format(module.name)) snackBarHost.showSnackbar(message.format(module.name))
} }
} }
@@ -611,7 +625,7 @@ fun ModuleItem(
onRestore: (ModuleViewModel.ModuleInfo) -> Unit, onRestore: (ModuleViewModel.ModuleInfo) -> Unit,
onCheckChanged: (Boolean) -> Unit, onCheckChanged: (Boolean) -> Unit,
onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, onUpdate: (ModuleViewModel.ModuleInfo) -> Unit,
onClick: (ModuleViewModel.ModuleInfo) -> Unit onClick: (ModuleViewModel.ModuleInfo) -> Unit,
) { ) {
ElevatedCard( ElevatedCard(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -620,7 +634,7 @@ fun ModuleItem(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val indication = LocalIndication.current val indication = LocalIndication.current
val viewModel = viewModel<ModuleViewModel>() val viewModel = viewModel<ModuleViewModel>()
val context = LocalContext.current val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
@@ -862,7 +876,7 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.labelMedium.fontSize, fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.uninstall) text = stringResource(R.string.uninstall)
) )
} }
} }
} }
} }
@@ -85,6 +85,7 @@ import com.rifsxd.ksunext.ui.util.getBugreportFile
import com.rifsxd.ksunext.ui.util.* import com.rifsxd.ksunext.ui.util.*
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import androidx.core.content.edit
/** /**
* @author weishu * @author weishu
@@ -162,7 +163,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
title = stringResource(id = R.string.settings_umount_modules_default), title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary), summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked checked = umountChecked
) { ) {
if (Natives.setDefaultUmountModules(it)) { if (Natives.setDefaultUmountModules(it)) {
umountChecked = 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) { if (ksuVersion != null) {
val backupRestore = stringResource(id = R.string.backup_restore) val backupRestore = stringResource(id = R.string.backup_restore)
ListItem( ListItem(
@@ -496,7 +514,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
@Composable @Composable
fun UninstallItem( fun UninstallItem(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
withLoading: suspend (suspend () -> Unit) -> Unit withLoading: suspend (suspend () -> Unit) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -598,7 +616,7 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar( private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null,
) { ) {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.settings)) }, title = { Text(stringResource(R.string.settings)) },
@@ -1,6 +1,9 @@
package com.rifsxd.ksunext.ui.theme package com.rifsxd.ksunext.ui.theme
import android.os.Build import android.os.Build
import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
@@ -8,6 +11,9 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable 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 androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
@@ -38,9 +44,42 @@ fun KernelSUTheme(
else -> LightColorScheme else -> LightColorScheme
} }
SystemBarStyle(
darkMode = darkTheme
)
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
content = content 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(),
)
}
)
}
}
@@ -1,11 +1,7 @@
package com.rifsxd.ksunext.ui.viewmodel 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.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
@@ -14,20 +10,16 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.topjohnwu.superuser.Shell import com.dergoogler.mmrl.platform.Platform
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.rifsxd.ksunext.IKsuInterface
import com.rifsxd.ksunext.Natives import com.rifsxd.ksunext.Natives
import com.rifsxd.ksunext.ksuApp import com.rifsxd.ksunext.ksuApp
import com.rifsxd.ksunext.ui.KsuService
import com.rifsxd.ksunext.ui.util.HanziToPinyin 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.text.Collator
import java.util.* import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class SuperUserViewModel : ViewModel() { 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() { suspend fun fetchAppList() {
isRefreshing = true isRefreshing = true
val result = connectKsuService {
Log.w(TAG, "KsuService disconnected")
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val binder = result.first val packages = Platform.getPackages(0).list
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
withContext(Dispatchers.Main) {
stopKsuService()
}
val packages = allPackages.list
apps = packages.map { apps = packages.map {
val appInfo = it.applicationInfo val appInfo = it.applicationInfo
@@ -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)
}
@@ -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)
}
}
@@ -15,6 +15,8 @@ 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.webkit.WebViewAssetLoader 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.topjohnwu.superuser.Shell
import com.rifsxd.ksunext.ui.util.createRootShell import com.rifsxd.ksunext.ui.util.createRootShell
import java.io.File import java.io.File
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
} else { } else {
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
setTaskDescription(taskDescription) setTaskDescription(taskDescription)
} }
@@ -62,7 +65,7 @@ class WebUIActivity : ComponentActivity() {
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)
} }
@@ -82,7 +85,9 @@ class WebUIActivity : ComponentActivity() {
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.domStorageEnabled = true settings.domStorageEnabled = true
settings.allowFileAccess = false settings.allowFileAccess = false
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir) webviewInterface = WebViewInterface(
WXOptions(this@WebUIActivity, this, ModId(moduleId))
)
addJavascriptInterface(webviewInterface, "ksu") addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient) setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html") loadUrl("https://mui.kernelsu.org/index.html")
@@ -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()
)
)
}
}
}
}
@@ -1,16 +1,17 @@
package com.rifsxd.ksunext.ui.webui package com.rifsxd.ksunext.ui.webui
import android.app.Activity import android.app.Activity
import android.content.Context
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.view.Window import android.view.Window
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebView
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.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.CallbackList
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -23,10 +24,15 @@ import java.io.File
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class WebViewInterface( class WebViewInterface(
val context: Context, wxOptions: WXOptions,
private val webView: WebView, ) : WebUIInterface(wxOptions) {
private val modDir: String override var name: String = "ksu"
) {
companion object {
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 {
@@ -59,7 +65,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)
@@ -168,9 +174,9 @@ class WebViewInterface(
if (context is Activity) { if (context is Activity) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (enable) { if (enable) {
hideSystemUI(context.window) hideSystemUI(activity.window)
} else { } else {
showSystemUI(context.window) showSystemUI(activity.window)
} }
} }
} }
@@ -202,8 +208,12 @@ class WebViewInterface(
fun hideSystemUI(window: Window) = 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) = fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) WindowInsetsControllerCompat(
window,
window.decorView
).show(WindowInsetsCompat.Type.systemBars())
@@ -194,4 +194,6 @@
<string name="settings_disable_su">Disable su compatibility</string> <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_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="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> </resources>
+7 -1
View File
@@ -17,6 +17,7 @@ parcelablelist = "2.0.1"
libsu = "6.0.0" libsu = "6.0.0"
apksign = "1.4" apksign = "1.4"
cmaker = "1.2" cmaker = "1.2"
mmrl = "785605169b"
[plugins] [plugins]
agp-app = { id = "com.android.application", version.ref = "agp" } 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" } 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" }