manager: Add module UI
This commit is contained in:
4
manager/.idea/compiler.xml
generated
4
manager/.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="19" />
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
2
manager/.idea/gradle.xml
generated
2
manager/.idea/gradle.xml
generated
@@ -7,7 +7,7 @@
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="19" />
|
||||
<option name="gradleJvm" value="Embedded JDK" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
2
manager/.idea/misc.xml
generated
2
manager/.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -76,6 +76,8 @@ dependencies {
|
||||
implementation("io.coil-kt:coil-compose:2.2.2")
|
||||
implementation("me.zhanghai.android.appiconloader:appiconloader-coil:1.5.0")
|
||||
|
||||
implementation("com.github.topjohnwu.libsu:core:5.0.3")
|
||||
|
||||
ksp("io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
|
||||
@@ -16,3 +16,5 @@ add_library(kernelsu
|
||||
find_library(log-lib log)
|
||||
|
||||
target_link_libraries(kernelsu ${log-lib})
|
||||
|
||||
add_executable(libksu.so su.c)
|
||||
@@ -1,10 +1,14 @@
|
||||
package me.weishu.kernelsu
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
@@ -24,5 +28,21 @@ class KernelSUApplication : Application() {
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
fun createRootShell(): Shell {
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||
val su = applicationInfo.nativeLibraryDir + File.separator + "libksu.so"
|
||||
val builder = Shell.Builder.create()
|
||||
return builder.build(su)
|
||||
}
|
||||
|
||||
fun install() {
|
||||
val shell = createRootShell()
|
||||
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||
val result = ShellUtils.fastCmdResult(shell, "$ksduLib install")
|
||||
Log.w("KernelSU", "install ksud result: $result")
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,229 @@
|
||||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun ModuleScreen() {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
Text(text = "Coming Soon..")
|
||||
val viewModel = viewModel<ModuleViewModel>()
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar()
|
||||
},
|
||||
floatingActionButton = {
|
||||
val moduleInstall = stringResource(id = R.string.module_install)
|
||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != RESULT_OK) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
viewModel.installModule(uri)
|
||||
}
|
||||
Log.i("ModuleScreen", "select zip result: ${it.data}")
|
||||
}
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
// select the zip file to install
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "application/zip"
|
||||
selectZipLauncher.launch(intent)
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, moduleInstall) },
|
||||
text = { Text(text = moduleInstall) },
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
val failedEnable = stringResource(R.string.module_failed_to_enable)
|
||||
val failedDisable = stringResource(R.string.module_failed_to_disable)
|
||||
val swipeState = rememberSwipeRefreshState(viewModel.isRefreshing)
|
||||
// TODO: Replace SwipeRefresh with RefreshIndicator when it's ready
|
||||
SwipeRefresh(
|
||||
state = swipeState,
|
||||
onRefresh = {
|
||||
scope.launch { viewModel.fetchModuleList() }
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val isEmpty = viewModel.moduleList.isEmpty()
|
||||
if (isEmpty) {
|
||||
swipeState.isRefreshing = false
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(R.string.module_empty))
|
||||
}
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(viewModel.moduleList) { module ->
|
||||
var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) }
|
||||
ModuleItem(module,
|
||||
isChecked,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
val result = viewModel.uninstallModule(module.id)
|
||||
}
|
||||
},
|
||||
onCheckChanged = {
|
||||
val success = viewModel.toggleModule(module.id, isChecked)
|
||||
if (success) {
|
||||
isChecked = it
|
||||
} else scope.launch {
|
||||
val message = if (isChecked) failedDisable else failedEnable
|
||||
snackBarHost.showSnackbar(message.format(module.name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.module)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleItem(
|
||||
module: ModuleViewModel.ModuleInfo,
|
||||
isChecked: Boolean,
|
||||
onUninstall: (ModuleViewModel.ModuleInfo) -> Unit,
|
||||
onCheckChanged: (Boolean) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
colors =
|
||||
CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)) {
|
||||
Row {
|
||||
Column {
|
||||
Text(
|
||||
fontSize = MaterialTheme.typography.titleLarge.fontSize,
|
||||
fontFamily = MaterialTheme.typography.titleLarge.fontFamily,
|
||||
text = module.name,
|
||||
)
|
||||
|
||||
Row {
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
text = module.version
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
text = module.author
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Switch(checked = isChecked, onCheckedChange = onCheckChanged)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
text = module.description,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
)
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Divider(thickness = Dp.Hairline)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f, true))
|
||||
|
||||
TextButton(
|
||||
onClick = { onUninstall(module) },
|
||||
) {
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.uninstall),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ModuleItemPreview() {
|
||||
val module = ModuleViewModel.ModuleInfo(
|
||||
id = "id",
|
||||
name = "name",
|
||||
version = "version",
|
||||
versionCode = 1,
|
||||
author = "author",
|
||||
description = "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a ",
|
||||
enabled = true
|
||||
)
|
||||
ModuleItem(module, true, {}, {})
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package me.weishu.kernelsu.ui.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.topjohnwu.superuser.ShellUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.weishu.kernelsu.ksuApp
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
|
||||
class ModuleViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ModuleViewModel"
|
||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||
}
|
||||
|
||||
class ModuleInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val author: String,
|
||||
val version: String,
|
||||
val versionCode: Int,
|
||||
val description: String,
|
||||
val enabled: Boolean
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val moduleList by derivedStateOf {
|
||||
val comparator = compareBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id)
|
||||
modules.sortedWith(comparator).also {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchModuleList() {
|
||||
withContext(Dispatchers.IO) {
|
||||
isRefreshing = true
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
val shell = ksuApp.createRootShell()
|
||||
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||
|
||||
val out = shell.newJob().add("$ksduLib module list").to(ArrayList(), null).exec().out
|
||||
val result = out.joinToString("\n")
|
||||
|
||||
Log.i(TAG, "result: $result")
|
||||
|
||||
val array = JSONArray(result)
|
||||
modules = (0 until array.length())
|
||||
.asSequence()
|
||||
.map { array.getJSONObject(it) }
|
||||
.map { obj ->
|
||||
ModuleInfo(
|
||||
obj.getString("id"),
|
||||
obj.getString("name"),
|
||||
obj.getString("author"),
|
||||
obj.getString("version"),
|
||||
obj.getInt("versionCode"),
|
||||
obj.getString("description"),
|
||||
obj.getBoolean("enabled")
|
||||
)
|
||||
}.toList()
|
||||
|
||||
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
|
||||
}
|
||||
}
|
||||
|
||||
private fun execKsud(args: String): Boolean {
|
||||
val shell = ksuApp.createRootShell()
|
||||
val ksduLib = ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||
return ShellUtils.fastCmdResult(shell, "$ksduLib $args")
|
||||
}
|
||||
|
||||
fun toggleModule(id: String, enable: Boolean): Boolean {
|
||||
val cmd = if (enable) {
|
||||
"module enable $id"
|
||||
} else {
|
||||
"module disable $id"
|
||||
}
|
||||
val result = execKsud(cmd)
|
||||
Log.i(TAG, "toggle module $id result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun uninstallModule(id: String) : Boolean {
|
||||
val cmd = "module uninstall $id"
|
||||
val result = execKsud(cmd)
|
||||
Log.i(TAG, "uninstall module $id result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun installModule(uri: Uri) : Boolean {
|
||||
val resolver = ksuApp.contentResolver
|
||||
with(resolver.openInputStream(uri)) {
|
||||
val file = File(ksuApp.cacheDir, "module.zip")
|
||||
file.outputStream().use { output ->
|
||||
this?.copyTo(output)
|
||||
}
|
||||
val cmd = "module install ${file.absolutePath}"
|
||||
val result = execKsud(cmd)
|
||||
Log.i(TAG, "install module $uri result: $result")
|
||||
|
||||
file.delete()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@
|
||||
|
||||
<string name="superuser">Superuser</string>
|
||||
<string name="superuser_failed_to_grant_root">Failed to grant root for %d</string>
|
||||
<string name="module_failed_to_enable">Failed to enable module: %s</string>
|
||||
<string name="module_failed_to_disable">Failed to disable module: %s</string>
|
||||
<string name="module_empty">No module installed</string>
|
||||
|
||||
<string name="module">Module</string>
|
||||
<string name="uninstall">Uninstall</string>
|
||||
<string name="module_install">Install</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
userspace/.gitignore
vendored
2
userspace/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
/obj
|
||||
/libs
|
||||
@@ -1,7 +0,0 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := su
|
||||
LOCAL_SRC_FILES := su.c
|
||||
|
||||
include $(BUILD_EXECUTABLE)
|
||||
@@ -1,3 +0,0 @@
|
||||
APP_ABI := arm64-v8a x86_64
|
||||
APP_PLATFORM := android-24
|
||||
APP_STL := none
|
||||
Reference in New Issue
Block a user