manager: support load module webui
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
6
manager/app/src/main/res/xml/network_config.xml
Normal file
6
manager/app/src/main/res/xml/network_config.xml
Normal 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>
|
||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user