From fceffc9cfe2cd305fd0356fbb332f0feae919db4 Mon Sep 17 00:00:00 2001 From: weishu Date: Tue, 20 Feb 2024 19:21:03 +0800 Subject: [PATCH] manager: support load module webui --- manager/app/build.gradle.kts | 1 + manager/app/src/main/AndroidManifest.xml | 1 + .../me/weishu/kernelsu/ui/screen/Module.kt | 34 ++++-- .../me/weishu/kernelsu/ui/screen/WebScreen.kt | 104 ++++++++++++++++++ .../java/me/weishu/kernelsu/ui/util/KsuCli.kt | 9 ++ .../kernelsu/ui/viewmodel/ModuleViewModel.kt | 4 +- .../app/src/main/res/xml/network_config.xml | 6 + manager/gradle/libs.versions.toml | 1 + 8 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 manager/app/src/main/java/me/weishu/kernelsu/ui/screen/WebScreen.kt create mode 100644 manager/app/src/main/res/xml/network_config.xml diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index 4463e4f0..b2757159 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { implementation(libs.com.google.accompanist.drawablepainter) implementation(libs.com.google.accompanist.navigation.animation) implementation(libs.com.google.accompanist.systemuicontroller) + implementation(libs.com.google.accompanist.webview) implementation(libs.compose.destinations.animations.core) ksp(libs.compose.destinations.ksp) diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index 96562cd8..6d434be1 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.KernelSU" + android:usesCleartextTraffic="true" tools:targetApi="33"> + if (hasWebUi) { + navigator.navigate(WebScreenDestination(id, name)) + } + }) } } } @@ -134,7 +141,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) { @OptIn(ExperimentalMaterialApi::class) @Composable private fun ModuleList( - viewModel: ModuleViewModel, modifier: Modifier = Modifier, onInstallModule: (Uri) -> Unit + viewModel: ModuleViewModel, + modifier: Modifier = Modifier, + onInstallModule: (Uri) -> Unit, + onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit ) { val failedEnable = stringResource(R.string.module_failed_to_enable) val failedDisable = stringResource(R.string.module_failed_to_disable) @@ -172,7 +182,7 @@ private fun ModuleList( } } - val showToast: suspend (String) -> Unit = {msg-> + val showToast: suspend (String) -> Unit = { msg -> withContext(Dispatchers.Main) { Toast.makeText( context, @@ -346,6 +356,8 @@ private fun ModuleList( "${module.name}-${updatedModule.second}.zip" ) } + }, onClick = { + onClickModule(it.id, it.name, it.hasWebUi) }) // fix last item shadow incomplete in LazyColumn @@ -379,9 +391,12 @@ private fun ModuleItem( onUninstall: (ModuleViewModel.ModuleInfo) -> Unit, onCheckChanged: (Boolean) -> Unit, onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, + onClick: (ModuleViewModel.ModuleInfo) -> Unit ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .clickable { onClick(module) }, colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface) ) { @@ -505,7 +520,8 @@ fun ModuleItemPreview() { enabled = true, update = true, remove = true, - updateJson = "" + updateJson = "", + hasWebUi = false, ) - ModuleItem(module, true, "", {}, {}, {}) + ModuleItem(module, true, "", {}, {}, {}, {}) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/WebScreen.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/WebScreen.kt new file mode 100644 index 00000000..eabad03f --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/WebScreen.kt @@ -0,0 +1,104 @@ +package me.weishu.kernelsu.ui.screen + +import android.annotation.SuppressLint +import android.webkit.JavascriptInterface +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.topjohnwu.superuser.ShellUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.weishu.kernelsu.ui.util.KsuCli +import me.weishu.kernelsu.ui.util.createRootShell +import me.weishu.kernelsu.ui.util.serveModule +import java.net.ServerSocket + + +@SuppressLint("SetJavaScriptEnabled") +@Destination +@Composable +fun WebScreen(navigator: DestinationsNavigator, moduleId: String, moduleName: String) { + + val port = 8080 + LaunchedEffect(Unit) { + serveModule(moduleId, port) + } + + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(Unit) { + onDispose { + lifecycleOwner.lifecycleScope.launch { + stopServer(port) + } + } + } + + Scaffold(topBar = { + TopBar(moduleName) + }) { innerPadding -> + WebView( + state = rememberWebViewState(url = "http://localhost:$port"), + Modifier + .fillMaxSize() + .padding(innerPadding), + factory = { context -> + android.webkit.WebView(context).apply { + settings.javaScriptEnabled = true + addJavascriptInterface(WebViewInterface(), "ksu") + } + }) + } +} + +class WebViewInterface { + @JavascriptInterface + fun exec(cmd: String): String { + val shell = createRootShell() + return ShellUtils.fastCmd(shell, cmd) + } + + @JavascriptInterface + fun launchApp(pkg: String) { + val context = me.weishu.kernelsu.ksuApp + context.packageManager.getLaunchIntentForPackage(pkg)?.let { + context.startActivity(it) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar(title: String) { + TopAppBar(title = { Text(title) }) +} + +private suspend fun getFreePort(): Int { + return withContext(Dispatchers.IO) { + ServerSocket(0).use { socket -> socket.localPort } + } +} + +private suspend fun stopServer(port: Int) { + withContext(Dispatchers.IO) { + runCatching { + okhttp3.OkHttpClient() + .newCall(okhttp3.Request.Builder().url("http://localhost:$port/stop").build()) + .execute() + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt index 56372779..e61eaf99 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt @@ -131,6 +131,15 @@ fun installModule( } } +fun serveModule(id: String, port: Int): Process { + // we should use a new root shell to avoid blocking the global shell + val process = Runtime.getRuntime().exec("${getKsuDaemonPath()} debug su") + val builder = Shell.Builder.create() + val shell = builder.build(process) + shell.newJob().add("${getKsuDaemonPath()} module serve $id $port").submit() + return process +} + fun reboot(reason: String = "") { val shell = getRootShell() if (reason == "recovery") { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 1d3bee0c..2d0fef8a 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -36,6 +36,7 @@ class ModuleViewModel : ViewModel() { val update: Boolean, val remove: Boolean, val updateJson: String, + val hasWebUi: Boolean, ) data class ModuleUpdateInfo( @@ -96,7 +97,8 @@ class ModuleViewModel : ViewModel() { obj.getBoolean("enabled"), obj.getBoolean("update"), obj.getBoolean("remove"), - obj.optString("updateJson") + obj.optString("updateJson"), + obj.optBoolean("web") ) }.toList() isNeedRefresh = false diff --git a/manager/app/src/main/res/xml/network_config.xml b/manager/app/src/main/res/xml/network_config.xml new file mode 100644 index 00000000..85285232 --- /dev/null +++ b/manager/app/src/main/res/xml/network_config.xml @@ -0,0 +1,6 @@ + + + + 127.0.0.1 + + \ No newline at end of file diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index c6be9065..bb737b24 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -39,6 +39,7 @@ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "l com-google-accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" } com-google-accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanist" } com-google-accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } +com-google-accompanist-webview = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" } com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } com-github-topjohnwu-libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }