manager: support load module webui

This commit is contained in:
weishu
2024-02-20 19:21:03 +08:00
parent 01b685ce58
commit fceffc9cfe
8 changed files with 150 additions and 10 deletions

View File

@@ -93,6 +93,7 @@ dependencies {
implementation(libs.com.google.accompanist.drawablepainter) implementation(libs.com.google.accompanist.drawablepainter)
implementation(libs.com.google.accompanist.navigation.animation) implementation(libs.com.google.accompanist.navigation.animation)
implementation(libs.com.google.accompanist.systemuicontroller) implementation(libs.com.google.accompanist.systemuicontroller)
implementation(libs.com.google.accompanist.webview)
implementation(libs.compose.destinations.animations.core) implementation(libs.compose.destinations.animations.core)
ksp(libs.compose.destinations.ksp) ksp(libs.compose.destinations.ksp)

View File

@@ -14,6 +14,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.KernelSU" android:theme="@style/Theme.KernelSU"
android:usesCleartextTraffic="true"
tools:targetApi="33"> tools:targetApi="33">
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"

View File

@@ -7,6 +7,7 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -43,6 +44,7 @@ import me.weishu.kernelsu.ui.component.ConfirmDialog
import me.weishu.kernelsu.ui.component.ConfirmResult import me.weishu.kernelsu.ui.component.ConfirmResult
import me.weishu.kernelsu.ui.component.LoadingDialog import me.weishu.kernelsu.ui.component.LoadingDialog
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -122,10 +124,15 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
ModuleList( ModuleList(
viewModel = viewModel, modifier = Modifier viewModel = viewModel, modifier = Modifier
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize() .fillMaxSize(),
) { onInstallModule =
navigator.navigate(InstallScreenDestination(it)) {
} navigator.navigate(InstallScreenDestination(it))
}, onClickModule = { id, name, hasWebUi ->
if (hasWebUi) {
navigator.navigate(WebScreenDestination(id, name))
}
})
} }
} }
} }
@@ -134,7 +141,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
private fun ModuleList( 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 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)
@@ -172,7 +182,7 @@ private fun ModuleList(
} }
} }
val showToast: suspend (String) -> Unit = {msg-> val showToast: suspend (String) -> Unit = { msg ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText( Toast.makeText(
context, context,
@@ -346,6 +356,8 @@ private fun ModuleList(
"${module.name}-${updatedModule.second}.zip" "${module.name}-${updatedModule.second}.zip"
) )
} }
}, onClick = {
onClickModule(it.id, it.name, it.hasWebUi)
}) })
// fix last item shadow incomplete in LazyColumn // fix last item shadow incomplete in LazyColumn
@@ -379,9 +391,12 @@ private fun ModuleItem(
onUninstall: (ModuleViewModel.ModuleInfo) -> Unit, onUninstall: (ModuleViewModel.ModuleInfo) -> Unit,
onCheckChanged: (Boolean) -> Unit, onCheckChanged: (Boolean) -> Unit,
onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, onUpdate: (ModuleViewModel.ModuleInfo) -> Unit,
onClick: (ModuleViewModel.ModuleInfo) -> Unit
) { ) {
ElevatedCard( ElevatedCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.clickable { onClick(module) },
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface) colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
) { ) {
@@ -505,7 +520,8 @@ fun ModuleItemPreview() {
enabled = true, enabled = true,
update = true, update = true,
remove = true, remove = true,
updateJson = "" updateJson = "",
hasWebUi = false,
) )
ModuleItem(module, true, "", {}, {}, {}) ModuleItem(module, true, "", {}, {}, {}, {})
} }

View File

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

View File

@@ -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 = "") { fun reboot(reason: String = "") {
val shell = getRootShell() val shell = getRootShell()
if (reason == "recovery") { if (reason == "recovery") {

View File

@@ -36,6 +36,7 @@ class ModuleViewModel : ViewModel() {
val update: Boolean, val update: Boolean,
val remove: Boolean, val remove: Boolean,
val updateJson: String, val updateJson: String,
val hasWebUi: Boolean,
) )
data class ModuleUpdateInfo( data class ModuleUpdateInfo(
@@ -96,7 +97,8 @@ class ModuleViewModel : ViewModel() {
obj.getBoolean("enabled"), obj.getBoolean("enabled"),
obj.getBoolean("update"), obj.getBoolean("update"),
obj.getBoolean("remove"), obj.getBoolean("remove"),
obj.optString("updateJson") obj.optString("updateJson"),
obj.optBoolean("web")
) )
}.toList() }.toList()
isNeedRefresh = false isNeedRefresh = false

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@@ -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-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-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-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-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" } com-github-topjohnwu-libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }