10 Commits

Author SHA1 Message Date
ShirkNeko
3d0d87cb0c Add application DPI setting function
- Allow users to customize the display density of the current application

- Fix some popup color errors
2025-05-13 23:56:18 +08:00
ShirkNeko
6b66d9b3f8 Remove redundant definitions of KPM strings
Clean up unused code in build scripts
2025-05-13 21:57:42 +08:00
ShirkNeko
a301d94858 [skip ci]: Fix missing brackets in KPM feature information summary 2025-05-13 21:51:21 +08:00
ShirkNeko
01199470f2 [manager]: Add KPM function display options and related settings
- Eruda injection web UI X will not be displayed when the modification is not enabled.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-13 21:44:42 +08:00
ShirkNeko
9e7ea19567 [skip ci]:Update some descriptions 2025-05-13 18:30:39 +08:00
Der_Googler
cdc6a6cb4a Add option to use WebUI X
- Added WebUI X from MMRL

Co-authored-by:Der_Googler <54764558+DerGoogler@users.noreply.github.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-13 15:44:20 +08:00
ShirkNeko
bb2d8fd7e0 Refactoring the KsuIsValid Check Logic
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
2025-05-12 00:06:17 +08:00
ShirkNeko
2b6d418fe6 [skip ci]:Removing unused LocalDensity variables 2025-05-11 23:36:09 +08:00
ShirkNeko
d8b1126b96 [skip ci]:Modify comments 2025-05-11 23:32:46 +08:00
ShirkNeko
2eeddcfa80 Modify UI layout
- Adjust the maximum number of SwitchItem rows to optimize the layout and spacing of interface buttons
2025-05-11 23:30:48 +08:00
30 changed files with 823 additions and 238 deletions

View File

@@ -154,4 +154,9 @@ dependencies {
implementation(libs.com.github.topjohnwu.libsu.core)
implementation(libs.mmrl.platform)
compileOnly(libs.mmrl.hidden.api)
implementation(libs.mmrl.webui)
implementation(libs.mmrl.ui)
}

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.sukisu.ultra.ui.webui.WebViewInterface { *; }
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }

View File

@@ -39,6 +39,13 @@
android:exported="false"
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
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@@ -3,6 +3,7 @@ package com.sukisu.ultra
import android.app.Application
import coil.Coil
import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File
@@ -15,6 +16,8 @@ class KernelSUApplication : Application() {
super.onCreate()
ksuApp = this
Platform.setHiddenApiExemptions()
val context = this
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
Coil.setImageLoader(

View File

@@ -90,14 +90,6 @@ object Natives {
fun requireNewKernel(): Boolean {
return version < MINIMAL_SUPPORTED_KERNEL
}
fun isKsuValid(pkgName: String?): Boolean {
if (becomeManager(pkgName)) {
return true
} else {
return false
}
}
@Immutable
@Parcelize

View File

@@ -1,77 +0,0 @@
package com.sukisu.ultra.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.sukisu.zako.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<>();
}
}

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui
import android.content.Context
import android.database.ContentObserver
import android.os.Build
import android.os.Bundle
@@ -13,8 +14,8 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
@@ -34,6 +35,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.webui.initPlatform
class MainActivity : ComponentActivity() {
private inner class ThemeChangeContentObserver(
@@ -47,6 +49,9 @@ class MainActivity : ComponentActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
// 应用DPI设置仅对当前应用生效
applyCustomDpi()
// Enable edge to edge
enableEdgeToEdge()
@@ -104,6 +109,11 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
// pre-init platform to faster start WebUI X activities
LaunchedEffect(Unit) {
initPlatform()
}
Scaffold(
bottomBar = { BottomBar(navController) },
contentWindowInsets = WindowInsets(0, 0, 0, 0)
@@ -128,10 +138,30 @@ class MainActivity : ComponentActivity() {
}
}
// 应用自定义DPI设置仅对当前应用生效
private fun applyCustomDpi() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try {
val resources = resources
val metrics = resources.displayMetrics
// 仅更新应用内显示,不影响系统状态栏
metrics.density = customDpi / 160f
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onPause() {
super.onPause()
CardConfig.save(applicationContext)
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() {
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", true)
}
ThemeConfig.preventBackgroundRefresh = true
@@ -162,6 +192,10 @@ private fun BottomBar(navController: NavHostController) {
val containerColor = MaterialTheme.colorScheme.surfaceVariant
val cardColor = MaterialTheme.colorScheme.surfaceVariant
// 检查是否显示KPM
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
NavigationBar(
modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
@@ -174,7 +208,7 @@ private fun BottomBar(navController: NavHostController) {
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo) {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(

View File

@@ -48,7 +48,6 @@ fun ImageEditorDialog(
var offsetY by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val density = LocalDensity.current
var lastScale by remember { mutableFloatStateOf(1f) }
var lastOffsetX by remember { mutableFloatStateOf(0f) }
var lastOffsetY by remember { mutableFloatStateOf(0f) }

View File

@@ -0,0 +1,17 @@
package com.sukisu.ultra.ui.component
import androidx.compose.runtime.Composable
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
@Composable
fun KsuIsValid(
content: @Composable () -> Unit
) {
val isManager = Natives.becomeManager(ksuApp.packageName)
val ksuVersion = if (isManager) Natives.version else null
if (ksuVersion != null) {
content()
}
}

View File

@@ -11,8 +11,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import com.dergoogler.mmrl.ui.component.LabelItem
import com.dergoogler.mmrl.ui.component.text.TextRow
@Composable
fun SwitchItem(
@@ -21,9 +24,11 @@ fun SwitchItem(
summary: String? = null,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
beta: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
ListItem(
modifier = Modifier
@@ -36,10 +41,30 @@ fun SwitchItem(
onValueChange = onCheckedChange
),
headlineContent = {
Text(title)
TextRow(
leadingContent = if (beta) {
{
LabelItem(
modifier = Modifier.then(stateAlpha),
text = "Beta"
)
}
} else null
) {
Text(
modifier = Modifier.then(stateAlpha),
text = title,
)
}
},
leadingContent = icon?.let {
{ Icon(icon, title) }
{
Icon(
modifier = Modifier.then(stateAlpha),
imageVector = icon,
contentDescription = title
)
}
},
trailingContent = {
Switch(
@@ -51,7 +76,10 @@ fun SwitchItem(
},
supportingContent = {
if (summary != null) {
Text(summary)
Text(
modifier = Modifier.then(stateAlpha),
text = summary
)
}
}
)
@@ -71,4 +99,4 @@ fun RadioItem(
RadioButton(selected = selected, onClick = onClick)
}
)
}
}

View File

@@ -23,7 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
/**
* 槽位选择对话框组件
* 用于HorizonKernel刷写时选择目标槽位
* 用于Kernel刷写时选择目标槽位
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -47,11 +47,7 @@ fun SlotSelectionDialog(
}
if (show) {
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
AlertDialog(
onDismissRequest = onDismiss,

View File

@@ -70,7 +70,7 @@ fun SwitchItem(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
maxLines = Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis
)
}

View File

@@ -95,6 +95,7 @@ import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
@@ -132,6 +133,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
var isHideLinkCard by rememberSaveable { mutableStateOf(false) }
var showKpmInfo by rememberSaveable { mutableStateOf(true) }
// 从 SharedPreferences 加载简洁模式状态
LaunchedEffect(Unit) {
@@ -149,6 +151,9 @@ fun HomeScreen(navigator: DestinationsNavigator) {
isHideLinkCard = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_link_card", false)
showKpmInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
}
val kernelVersion = getKernelVersion()
@@ -400,17 +405,18 @@ private fun TopBar(
}
var showDropdown by remember { mutableStateOf(false) }
if (Natives.isKsuValid(ksuApp.packageName)) {
IconButton(onClick = { showDropdown = true }) {
KsuIsValid {
IconButton(onClick = {
showDropdown = true
}) {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(R.string.reboot),
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(id = R.string.reboot)
)
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }
) {
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
RebootDropdownItem(id = R.string.reboot)
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@@ -485,6 +491,9 @@ private fun StatusCard(
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false)
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
Icon(
Icons.Outlined.CheckCircle,
contentDescription = stringResource(R.string.home_working),
@@ -524,7 +533,7 @@ private fun StatusCard(
)
val kpmVersion = getKpmVersion()
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo) {
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
@@ -652,7 +661,7 @@ fun WarningCard(
@Composable
fun ContributionCard() {
val uriHandler = LocalUriHandler.current
val links = listOf("https://github.com/zako", "https://github.com/udochina")
val links = listOf("https://github.com/ShirkNeko", "https://github.com/udochina")
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
@@ -788,6 +797,8 @@ private fun InfoCard() {
val context = LocalContext.current
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_simple_mode", false)
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHighest),
@@ -889,22 +900,25 @@ private fun InfoCard() {
val kpmVersion = getKpmVersion()
val isKpmConfigured = checkKpmConfigured()
val displayVersion = if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
val statusText = if (isKpmConfigured) {
stringResource(R.string.kernel_patched)
// 根据showKpmInfo决定是否显示KPM信息
if (showKpmInfo) {
val displayVersion = if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
val statusText = if (isKpmConfigured) {
stringResource(R.string.kernel_patched)
} else {
stringResource(R.string.kernel_not_enabled)
}
"${stringResource(R.string.not_supported)} ($statusText)"
} else {
stringResource(R.string.kernel_not_enabled)
"${stringResource(R.string.supported)} ($kpmVersion)"
}
"${stringResource(R.string.not_supported)} ($statusText)"
} else {
"${stringResource(R.string.supported)} ($kpmVersion)"
}
InfoCardItem(
stringResource(R.string.home_kpm_version),
displayVersion,
icon = Icons.Default.Code
)
InfoCardItem(
stringResource(R.string.home_kpm_version),
displayVersion,
icon = Icons.Default.Code
)
}
}
}

View File

@@ -68,10 +68,11 @@ import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.zip.ZipInputStream
import androidx.core.content.edit
import androidx.core.net.toUri
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.webui.WebUIXActivity
import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri
@OptIn(ExperimentalMaterial3Api::class)
@@ -395,10 +396,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi ->
if (hasWebUi) {
webUILauncher.launch(
Intent(context, WebUIActivity::class.java)
.setData("kernelsu://webui/$id".toUri())
.putExtra("id", id)
.putExtra("name", name)
if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
Intent(context, WebUIXActivity::class.java)
.setData("kernelsu://webuix/$id".toUri())
.putExtra("id", id)
.putExtra("name", name)
} else {
Intent(context, WebUIActivity::class.java)
.setData("kernelsu://webui/$id".toUri())
.putExtra("id", id)
.putExtra("name", name)
}
)
}
},

View File

@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
@@ -29,6 +30,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.NavigateNext
import androidx.compose.material.icons.filled.AcUnit
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.DarkMode
@@ -81,10 +83,9 @@ import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SwitchItem
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeColors
@@ -175,6 +176,17 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
isHideOtherInfo = newValue
}
// 显示KPM开关状态
var isShowKpmInfo by remember {
mutableStateOf(prefs.getBoolean("show_kpm_info", true))
}
// 更新显示KPM开关状态
val onShowKpmInfoChange = { newValue: Boolean ->
prefs.edit { putBoolean("show_kpm_info", newValue) }
isShowKpmInfo = newValue
}
// 隐藏SuSFS状态开关状态
var isHideSusfsStatus by remember {
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
@@ -215,6 +227,36 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
var isCustomizeExpanded by remember { mutableStateOf(false) }
var isAppearanceExpanded by remember { mutableStateOf(true) }
var isAdvancedExpanded by remember { mutableStateOf(false) }
var isDpiExpanded by remember { mutableStateOf(false) }
// DPI 设置
val systemDpi = remember { context.resources.displayMetrics.densityDpi }
var currentDpi by remember {
mutableIntStateOf(prefs.getInt("app_dpi", systemDpi))
}
var tempDpi by remember { mutableIntStateOf(currentDpi) }
var isDpiCustom by remember { mutableStateOf(true) }
var showDpiConfirmDialog by remember { mutableStateOf(false) }
// 预设 DPI 选项
val dpiPresets = mapOf(
stringResource(R.string.dpi_size_small) to 240,
stringResource(R.string.dpi_size_medium) to 320,
stringResource(R.string.dpi_size_large) to 420,
stringResource(R.string.dpi_size_extra_large) to 560
)
// 获取DPI大小
@Composable
fun getDpiFriendlyName(dpi: Int): String {
return when (dpi) {
240 -> stringResource(R.string.dpi_size_small)
320 -> stringResource(R.string.dpi_size_medium)
420 -> stringResource(R.string.dpi_size_large)
560 -> stringResource(R.string.dpi_size_extra_large)
else -> stringResource(R.string.dpi_size_custom)
}
}
// 初始化卡片配置
LaunchedEffect(Unit) {
@@ -251,10 +293,37 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
CardConfig.setDarkModeDefaults()
}
// 保存设置
currentDpi = prefs.getInt("app_dpi", systemDpi)
tempDpi = currentDpi
CardConfig.save(context)
}
// 应用 DPI 设置
val applyDpiSetting = { dpi: Int ->
if (dpi != currentDpi) {
// 保存到 SharedPreferences
prefs.edit {
putInt("app_dpi", dpi)
}
// 只修改应用级别的DPI设置
currentDpi = dpi
tempDpi = dpi
Toast.makeText(
context,
context.getString(R.string.dpi_applied_success, dpi),
Toast.LENGTH_SHORT
).show()
val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(restartIntent)
showDpiConfirmDialog = false
}
}
// 主题色选项
val themeColorOptions = listOf(
stringResource(R.string.color_default) to ThemeColors.Default,
@@ -267,7 +336,6 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
)
var showThemeColorDialog by remember { mutableStateOf(false) }
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
// 图片选择器
val pickImageLauncher = rememberLauncherForActivityResult(
@@ -564,6 +632,114 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
}
}
// DPI 设置部分
SectionHeader(
title = stringResource(R.string.dpi_settings),
expanded = isDpiExpanded,
onToggle = { isDpiExpanded = !isDpiExpanded }
)
AnimatedVisibility(
visible = isDpiExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Surface(
shape = RoundedCornerShape(16.dp),
tonalElevation = 1.dp,
modifier = Modifier.padding(bottom = 16.dp)
) {
Column {
ListItem(
headlineContent = { Text(stringResource(R.string.app_dpi_title)) },
supportingContent = { Text(stringResource(R.string.app_dpi_summary)) },
leadingContent = {
Icon(
Icons.Default.AcUnit,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
trailingContent = {
Text(
text = getDpiFriendlyName(tempDpi),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
)
)
// DPI 滑动条
Column(modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)) {
Slider(
value = tempDpi.toFloat(),
onValueChange = {
tempDpi = it.toInt()
isDpiCustom = !dpiPresets.containsValue(tempDpi)
},
valueRange = 160f..600f,
steps = 11,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
) {
dpiPresets.forEach { (name, dpi) ->
TextButton(
onClick = {
tempDpi = dpi
isDpiCustom = false
},
modifier = Modifier.weight(1f)
) {
Text(
text = name,
color = if (tempDpi == dpi)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
TextButton(
onClick = {
if (tempDpi != currentDpi) {
showDpiConfirmDialog = true
}
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Text(stringResource(R.string.dpi_apply_settings))
}
Text(
text = if (isDpiCustom)
"${stringResource(R.string.dpi_size_custom)}: $tempDpi"
else
"${getDpiFriendlyName(tempDpi)}: $tempDpi",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
}
// 自定义设置部分
SectionHeader(
title = stringResource(R.string.custom_settings),
@@ -642,6 +818,21 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
color = MaterialTheme.colorScheme.outlineVariant
)
// 显示KPM开关
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.show_kpm_info),
summary = stringResource(R.string.show_kpm_info_summary),
checked = isShowKpmInfo
) {
onShowKpmInfoChange(it)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 隐藏链接信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
@@ -674,7 +865,7 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
) {
Column {
// SELinux 开关
if (ksuIsValid) {
KsuIsValid {
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.selinux),
@@ -837,6 +1028,42 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
)
}
// DPI 设置确认对话框
if (showDpiConfirmDialog) {
AlertDialog(
onDismissRequest = { showDpiConfirmDialog = false },
title = { Text(stringResource(R.string.dpi_confirm_title)) },
text = {
Column {
Text(stringResource(R.string.dpi_confirm_message, currentDpi, tempDpi))
Text(
stringResource(R.string.dpi_confirm_summary),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
}
},
confirmButton = {
TextButton(
onClick = { applyDpiSetting(tempDpi) }
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(
onClick = {
showDpiConfirmDialog = false
tempDpi = currentDpi
}
) {
Text(stringResource(R.string.cancel))
}
}
)
}
// 主题色选择对话框
if (showThemeColorDialog) {
AlertDialog(

View File

@@ -6,6 +6,11 @@ import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
@@ -57,6 +62,8 @@ import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid
import com.dergoogler.mmrl.platform.Platform
@OptIn(ExperimentalMaterial3Api::class)
@@ -65,7 +72,6 @@ import java.time.format.DateTimeFormatter
fun SettingScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
Scaffold(
topBar = {
@@ -80,7 +86,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
AboutDialog(it)
}
val loadingDialog = rememberLoadingDialog()
// endregion
Column(
modifier = Modifier
@@ -88,12 +93,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
) {
// region 上下文与协程
val context = LocalContext.current
val scope = rememberCoroutineScope()
// endregion
// region 日志导出功能
val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? ->
@@ -110,7 +111,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
}
// 设置分组卡片 - 配置
// 配置
Card(
modifier = Modifier
.fillMaxWidth()
@@ -130,7 +131,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
// 配置文件模板入口
val profileTemplate = stringResource(id = R.string.settings_profile_template)
if (ksuIsValid) {
KsuIsValid {
SettingItem(
icon = Icons.Filled.Fence,
title = profileTemplate,
@@ -146,7 +147,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
mutableStateOf(Natives.isDefaultUmountModules())
}
if (ksuIsValid) {
KsuIsValid {
SwitchSettingItem(
icon = Icons.Filled.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default),
@@ -161,7 +162,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
// SU 禁用开关(仅在兼容版本显示)
if (ksuIsValid) {
KsuIsValid {
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled())
@@ -183,7 +184,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
}
// 设置分组卡片 - 应用设置
// 应用设置
Card(
modifier = Modifier
.fillMaxWidth()
@@ -226,7 +227,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
prefs.getBoolean("enable_web_debugging", false)
)
}
if (Natives.isKsuValid(ksuApp.packageName)) {
KsuIsValid {
SwitchSettingItem(
icon = Icons.Filled.DeveloperMode,
title = stringResource(id = R.string.enable_web_debugging),
@@ -239,6 +240,52 @@ fun SettingScreen(navigator: DestinationsNavigator) {
)
}
// Web X 开关
var useWebUIX by rememberSaveable {
mutableStateOf(
prefs.getBoolean("use_webuix", false)
)
}
KsuIsValid {
SwitchItem(
beta = true,
enabled = Platform.isAlive,
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) }
useWebUIX = it
}
}
// Web X Eruda 开关
var useWebUIXEruda by rememberSaveable {
mutableStateOf(
prefs.getBoolean("use_webuix_eruda", false)
)
}
KsuIsValid {
AnimatedVisibility(
visible = useWebUIX && enableWebDebugging,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SwitchItem(
beta = true,
enabled = Platform.isAlive && useWebUIX && enableWebDebugging,
icon = Icons.Filled.FormatListNumbered,
title = stringResource(id = R.string.use_webuix_eruda),
summary = stringResource(id = R.string.use_webuix_eruda_summary),
checked = useWebUIXEruda
) {
prefs.edit { putBoolean("use_webuix_eruda", it) }
useWebUIXEruda = it
}
}
}
// 更多设置
SettingItem(
icon = Icons.Filled.Settings,
@@ -251,7 +298,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
}
// 设置分组卡片 - 工具
// 工具
Card(
modifier = Modifier
.fillMaxWidth()

View File

@@ -166,7 +166,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
snackbarHost = { SnackbarHost(snackBarHostState) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
bottomBar = {
// 批量操作按钮,直接放在底部栏
AnimatedVisibility(
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
enter = slideInVertically(initialOffsetY = { it }),
@@ -181,15 +180,13 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
horizontalArrangement = Arrangement.SpaceEvenly
) {
OutlinedButton(
onClick = {
// 修改为重新赋值为空集合
viewModel.selectedApps = emptySet()
viewModel.showBatchActions = false
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
@@ -197,9 +194,9 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(android.R.string.cancel))
}
@@ -209,7 +206,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
viewModel.updateBatchPermissions(true)
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
@@ -217,9 +213,9 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(R.string.batch_authorization))
}
@@ -229,7 +225,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
viewModel.updateBatchPermissions(false)
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
@@ -237,9 +232,9 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
Icon(
imageVector = Icons.Filled.Block,
contentDescription = null,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(R.string.batch_cancel_authorization))
}
}

View File

@@ -44,6 +44,11 @@ import androidx.core.content.edit
import androidx.core.net.toUri
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
/**
* 主题配置对象,管理应用的主题相关状态
@@ -110,6 +115,10 @@ fun KernelSUTheme(
}
}
SystemBarStyle(
darkMode = darkTheme
)
// 初始加载配置
LaunchedEffect(Unit) {
context.loadThemeMode()
@@ -535,4 +544,36 @@ fun Context.loadDynamicColorState() {
.getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled
}
/**
* webui X样式
*/
@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(),
)
}
)
}
}

View File

@@ -1,11 +1,7 @@
package com.sukisu.ultra.ui.viewmodel
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.Parcelable
import android.os.SystemClock
import android.util.Log
@@ -14,22 +10,23 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import com.sukisu.zako.IKsuInterface
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.KsuService
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.KsuCli
import java.text.Collator
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
import com.sukisu.ultra.ui.webui.packageManager
import com.sukisu.ultra.ui.webui.userManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive
companion object {
private const val TAG = "SuperUserViewModel"
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
@@ -142,55 +139,28 @@ class SuperUserViewModel : ViewModel() {
fetchAppList() // 刷新列表以显示最新状态
}
private suspend fun connectKsuService(
onDisconnect: () -> Unit = {}
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect()
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
continuation.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() {
isRefreshing = true
val result = connectKsuService {
Log.w(TAG, "KsuService disconnected")
}
withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) {
while (!isPlatformAlive) {
delay(500)
}
} ?: return@withContext // Exit early if timeout
val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime()
val binder = result.first
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
val userInfos = Platform.userManager.getUsers()
val packages = mutableListOf<PackageInfo>()
val packageManager = Platform.packageManager
withContext(Dispatchers.Main) {
stopKsuService()
for (userInfo in userInfos) {
Log.i(TAG, "fetchAppList: ${userInfo.id}")
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
}
val packages = allPackages.list
apps = packages.map {
val appInfo = it.applicationInfo
val uid = appInfo!!.uid

View File

@@ -0,0 +1,62 @@
package com.sukisu.ultra.ui.webui
import android.content.ServiceConnection
import android.util.Log
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
import com.dergoogler.mmrl.platform.model.IProvider
import com.dergoogler.mmrl.platform.model.PlatformIntent
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.Natives
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
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)
}
}
// webui x
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)
}
return@withContext active
} catch (e: Exception) {
Log.e("KsuLibSu", "Failed to initialize platform", e)
return@withContext false
}
}
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService)
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService)

View File

@@ -0,0 +1,14 @@
package com.sukisu.ultra.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)
}
}

View File

@@ -15,9 +15,11 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.topjohnwu.superuser.Shell
import com.sukisu.ultra.ui.util.createRootShell
import java.io.File
import com.dergoogler.mmrl.webui.interfaces.WXOptions
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
} else {
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
setTaskDescription(taskDescription)
}
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html")

View File

@@ -0,0 +1,113 @@
package com.sukisu.ultra.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.platform.model.ModId
import com.dergoogler.mmrl.ui.component.Loading
import com.dergoogler.mmrl.webui.screen.WebUIScreen
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.ui.theme.KernelSUTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class WebUIXActivity : ComponentActivity() {
private lateinit var webView: WebView
private val userAgent
get(): String {
val ksuVersion = BuildConfig.VERSION_CODE
val platform = Platform.get("Unknown") {
platform.name
}
val platformVersion = Platform.get(-1) {
moduleManager.versionCode
}
val osVersion = Build.VERSION.RELEASE
val deviceModel = Build.MODEL
return "SukiSU /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
}
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 erudaInject = prefs.getBoolean("use_webuix_eruda", false)
val dark = isSystemInDarkTheme()
val options = rememberWebUIOptions(
modId = ModId(moduleId),
debug = webDebugging,
appVersionCode = BuildConfig.VERSION_CODE,
isDarkMode = dark,
enableEruda = erudaInject,
cls = WebUIXActivity::class.java,
userAgentString = userAgent
)
WebUIScreen(
webView = webView,
options = options,
interfaces = listOf(
WebViewInterface.factory()
)
)
}
}
}
}

View File

@@ -1,16 +1,17 @@
package com.sukisu.ultra.ui.webui
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import androidx.core.view.WindowInsetsCompat
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.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -25,10 +26,15 @@ import java.io.File
import java.util.concurrent.CompletableFuture
class WebViewInterface(
val context: Context,
private val webView: WebView,
private val modDir: String
) {
wxOptions: WXOptions,
) : WebUIInterface(wxOptions) {
override var name: String = "ksu"
companion object {
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
}
private val modDir get() = "/data/adb/modules/${modId.id}"
@JavascriptInterface
fun exec(cmd: String): String {
@@ -170,9 +176,9 @@ class WebViewInterface(
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(context.window)
hideSystemUI(activity.window)
} else {
showSystemUI(context.window)
showSystemUI(activity.window)
}
}
}

View File

@@ -226,7 +226,6 @@
<string name="batch_cancel_authorization">認証を一括でキャンセル</string>
<string name="backup">バックアップ</string>
<string name="color_yellow">イエロー</string>
<string name="kpm">カーネルモジュール</string>
<string name="kpm_title">カーネルモジュール</string>
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
<string name="kpm_version">バージョン</string>

View File

@@ -222,7 +222,6 @@
<string name="batch_cancel_authorization">Hủy ủy quyền hàng loạt</string>
<string name="backup">Sao lưu</string>
<string name="color_yellow">Vàng</string>
<string name="kpm">Mô-đun kpm</string>
<string name="kpm_title">Mô-đun KPM</string>
<string name="kpm_empty">Không có mô-đun nào được cài đặt.</string>
<string name="kpm_version">Phát hành</string>

View File

@@ -225,7 +225,6 @@
<string name="batch_authorization">授权</string>
<string name="batch_cancel_authorization">撤销</string>
<string name="color_yellow">黄色</string>
<string name="kpm">内核模块</string>
<string name="kpm_title">内核模块</string>
<string name="kpm_empty">暂无已安装的内核模块</string>
<string name="kpm_version">版本</string>
@@ -321,4 +320,26 @@
<string name="background_set_success">背景设置成功</string>
<string name="background_removed">已移除自定义背景</string>
<string name="root_require_for_install">需要 root 权限</string>
<!-- KPM 显示设置相关 -->
<string name="show_kpm_info">显示 KPM 功能</string>
<string name="show_kpm_info_summary">在主页和底栏显示 KPM 相关功能和信息 (需要重新打开应用)</string>
<!-- Webui X 设置相关 -->
<string name="use_webuix">使用 WebUI X</string>
<string name="use_webuix_summary">使用支持更多 API 的 WebUI X 而不是 WebUI</string>
<string name="use_webuix_eruda">将 Eruda 注入 WebUI X</string>
<string name="use_webuix_eruda_summary">在 WebUI X 中注入调试控制台,使调试更容易,需要启用 WebView 调试</string>
<!-- DPI设置相关字符串 -->
<string name="dpi_settings">DPI 设置</string>
<string name="app_dpi_title">应用DPI</string>
<string name="app_dpi_summary">仅调整当前应用的屏幕显示密度</string>
<string name="dpi_size_small"></string>
<string name="dpi_size_medium"></string>
<string name="dpi_size_large"></string>
<string name="dpi_size_extra_large">超大</string>
<string name="dpi_size_custom">自定义</string>
<string name="dpi_apply_settings">应用DPI设置</string>
<string name="dpi_confirm_title">确认更改DPI</string>
<string name="dpi_confirm_message">你确定要将应用DPI从 %1$d 更改为 %2$d 吗?</string>
<string name="dpi_confirm_summary">应用需要重启以应用新的DPI设置不会影响系统状态栏或其他应用</string>
<string name="dpi_applied_success">DPI 已设置为 %1$d重启应用后生效</string>
</resources>

View File

@@ -222,12 +222,11 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="failed_reboot">Reboot Failed</string>
<string name="batch_authorization">authorizations</string>
<string name="batch_authorization">empower</string>
<string name="batch_cancel_authorization">withdraw</string>
<string name="backup">Backup</string>
<string name="color_yellow">Yellow</string>
<string name="kpm">KPM</string>
<string name="kpm_title">Kernel Module</string>
<string name="kpm_title">KPM</string>
<string name="kpm_empty">No installed kernel modules at this time</string>
<string name="kpm_version">Version</string>
<string name="kpm_author">Author</string>
@@ -325,4 +324,26 @@
<string name="background_set_success">Background set successfully</string>
<string name="background_removed">Removed custom backgrounds</string>
<string name="root_require_for_install">Requires root privileges</string>
<!-- KPM display settings -->
<string name="show_kpm_info">Display KPM Function</string>
<string name="show_kpm_info_summary">Display KPM information and Function in home and bottom bar (Need to reopen the app)</string>
<!-- Webui X settings -->
<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>
<string name="use_webuix_eruda">Inject Eruda into WebUI X</string>
<string name="use_webuix_eruda_summary">Inject a debug console into WebUI X to make debugging easier. Requires web debugging to be on.</string>
<!-- DPI setting related strings -->
<string name="dpi_settings">DPI setting</string>
<string name="app_dpi_title">Applied DPI</string>
<string name="app_dpi_summary">Adjust the screen display density for the current application only</string>
<string name="dpi_size_small">Small </string>
<string name="dpi_size_medium">Medium </string>
<string name="dpi_size_large">Big</string>
<string name="dpi_size_extra_large">oversize</string>
<string name="dpi_size_custom">customizable</string>
<string name="dpi_apply_settings">Applying DPI settings</string>
<string name="dpi_confirm_title">Confirm DPI change</string>
<string name="dpi_confirm_message">Are you sure you want to change the application DPI from %1$d to %2$d?</string>
<string name="dpi_confirm_summary">Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications</string>
<string name="dpi_applied_success">DPI has been set to %1$d, effective after restarting the application</string>
</resources>

View File

@@ -1,7 +1,6 @@
import com.android.build.api.dsl.ApplicationDefaultConfig
import com.android.build.api.dsl.CommonExtension
import com.android.build.gradle.api.AndroidBasePlugin
import java.io.ByteArrayOutputStream
plugins {
alias(libs.plugins.agp.app) apply false
@@ -48,18 +47,6 @@ fun getGitDescribe(): String {
}.standardOutput.asText.get().trim()
}
fun getVersionCode(): Int {
val commitCount = getGitCommitCount()
val major = 1
return major * 10000 + commitCount + 606
}
fun getVersionName(): String {
return getGitDescribe()
}
subprojects {
plugins.withType(AndroidBasePlugin::class.java) {
extensions.configure(CommonExtension::class.java) {

View File

@@ -22,6 +22,7 @@ compose-material3 = "1.3.2"
compose-ui = "1.8.0"
compose-foundation = "1.7.8"
documentfile = "1.0.1"
mmrl = "v33560"
[plugins]
agp-app = { id = "com.android.application", version.ref = "agp" }
@@ -81,4 +82,10 @@ sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs"
markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" }
lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
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" }