6 Commits
v3.1 ... v3.1.1

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
25 changed files with 776 additions and 193 deletions

View File

@@ -154,4 +154,9 @@ dependencies {
implementation(libs.com.github.topjohnwu.libsu.core) 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:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" /> 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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"

View File

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

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

View File

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

View File

@@ -47,11 +47,7 @@ fun SlotSelectionDialog(
} }
if (show) { if (show) {
val cardColor = if (!ThemeConfig.useDynamicColor) { val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,

View File

@@ -133,6 +133,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) } var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) } var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
var isHideLinkCard by rememberSaveable { mutableStateOf(false) } var isHideLinkCard by rememberSaveable { mutableStateOf(false) }
var showKpmInfo by rememberSaveable { mutableStateOf(true) }
// 从 SharedPreferences 加载简洁模式状态 // 从 SharedPreferences 加载简洁模式状态
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -150,6 +151,9 @@ fun HomeScreen(navigator: DestinationsNavigator) {
isHideLinkCard = context.getSharedPreferences("settings", Context.MODE_PRIVATE) isHideLinkCard = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_link_card", false) .getBoolean("is_hide_link_card", false)
showKpmInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
} }
val kernelVersion = getKernelVersion() val kernelVersion = getKernelVersion()
@@ -487,6 +491,9 @@ private fun StatusCard(
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false) .getBoolean("is_hide_susfs_status", false)
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
Icon( Icon(
Icons.Outlined.CheckCircle, Icons.Outlined.CheckCircle,
contentDescription = stringResource(R.string.home_working), contentDescription = stringResource(R.string.home_working),
@@ -526,7 +533,7 @@ private fun StatusCard(
) )
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) { if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()), text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
@@ -790,6 +797,8 @@ private fun InfoCard() {
val context = LocalContext.current val context = LocalContext.current
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_simple_mode", false) .getBoolean("is_simple_mode", false)
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHighest), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHighest),
@@ -891,22 +900,25 @@ private fun InfoCard() {
val kpmVersion = getKpmVersion() val kpmVersion = getKpmVersion()
val isKpmConfigured = checkKpmConfigured() val isKpmConfigured = checkKpmConfigured()
val displayVersion = if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) { // 根据showKpmInfo决定是否显示KPM信息
val statusText = if (isKpmConfigured) { if (showKpmInfo) {
stringResource(R.string.kernel_patched) 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 { } 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( InfoCardItem(
stringResource(R.string.home_kpm_version), stringResource(R.string.home_kpm_version),
displayVersion, displayVersion,
icon = Icons.Default.Code icon = Icons.Default.Code
) )
}
} }
} }

View File

@@ -68,9 +68,11 @@ import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation 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) @OptIn(ExperimentalMaterial3Api::class)
@@ -394,10 +396,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi -> onClickModule = { id, name, hasWebUi ->
if (hasWebUi) { if (hasWebUi) {
webUILauncher.launch( webUILauncher.launch(
Intent(context, WebUIActivity::class.java) if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
.setData("kernelsu://webui/$id".toUri()) Intent(context, WebUIXActivity::class.java)
.putExtra("id", id) .setData("kernelsu://webuix/$id".toUri())
.putExtra("name", name) .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 package com.sukisu.ultra.ui.screen
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
@@ -29,6 +30,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.NavigateNext 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.Brush
import androidx.compose.material.icons.filled.ColorLens import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.DarkMode
@@ -81,9 +83,7 @@ import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.ImageEditorDialog import com.sukisu.ultra.ui.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SwitchItem import com.sukisu.ultra.ui.component.SwitchItem
@@ -176,6 +176,17 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
isHideOtherInfo = newValue 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状态开关状态 // 隐藏SuSFS状态开关状态
var isHideSusfsStatus by remember { var isHideSusfsStatus by remember {
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
@@ -216,6 +227,36 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
var isCustomizeExpanded by remember { mutableStateOf(false) } var isCustomizeExpanded by remember { mutableStateOf(false) }
var isAppearanceExpanded by remember { mutableStateOf(true) } var isAppearanceExpanded by remember { mutableStateOf(true) }
var isAdvancedExpanded by remember { mutableStateOf(false) } 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) { LaunchedEffect(Unit) {
@@ -252,10 +293,37 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
CardConfig.setDarkModeDefaults() CardConfig.setDarkModeDefaults()
} }
// 保存设置 currentDpi = prefs.getInt("app_dpi", systemDpi)
tempDpi = currentDpi
CardConfig.save(context) 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( val themeColorOptions = listOf(
stringResource(R.string.color_default) to ThemeColors.Default, stringResource(R.string.color_default) to ThemeColors.Default,
@@ -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( SectionHeader(
title = stringResource(R.string.custom_settings), title = stringResource(R.string.custom_settings),
@@ -642,6 +818,21 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
color = MaterialTheme.colorScheme.outlineVariant 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( SwitchItem(
icon = Icons.Filled.VisibilityOff, icon = Icons.Filled.VisibilityOff,
@@ -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) { if (showThemeColorDialog) {
AlertDialog( AlertDialog(

View File

@@ -6,6 +6,11 @@ import android.net.Uri
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.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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@@ -58,6 +63,7 @@ import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.dergoogler.mmrl.platform.Platform
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -80,7 +86,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
AboutDialog(it) AboutDialog(it)
} }
val loadingDialog = rememberLoadingDialog() val loadingDialog = rememberLoadingDialog()
// endregion
Column( Column(
modifier = Modifier modifier = Modifier
@@ -88,12 +93,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// region 上下文与协程
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// endregion
// region 日志导出功能
val exportBugreportLauncher = rememberLauncherForActivityResult( val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip") ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? -> ) { uri: Uri? ->
@@ -183,7 +184,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
} }
// 设置分组卡片 - 应用设置 // 应用设置
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -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( SettingItem(
icon = Icons.Filled.Settings, icon = Icons.Filled.Settings,
@@ -251,7 +298,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
} }
// 设置分组卡片 - 工具 // 工具
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -44,6 +44,11 @@ import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
import com.sukisu.ultra.ui.util.BackgroundTransformation import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground 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) { LaunchedEffect(Unit) {
context.loadThemeMode() context.loadThemeMode()
@@ -535,4 +544,36 @@ fun Context.loadDynamicColorState() {
.getBoolean("use_dynamic_color", true) .getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled 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 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.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
@@ -14,22 +10,23 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.sukisu.zako.IKsuInterface
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.KsuService
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.KsuCli
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import kotlin.coroutines.resume import com.dergoogler.mmrl.platform.Platform
import kotlin.coroutines.suspendCoroutine 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() { class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive
companion object { companion object {
private const val TAG = "SuperUserViewModel" private const val TAG = "SuperUserViewModel"
private var apps by mutableStateOf<List<AppInfo>>(emptyList()) private var apps by mutableStateOf<List<AppInfo>>(emptyList())
@@ -142,55 +139,28 @@ class SuperUserViewModel : ViewModel() {
fetchAppList() // 刷新列表以显示最新状态 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() { suspend fun fetchAppList() {
isRefreshing = true isRefreshing = true
val result = connectKsuService {
Log.w(TAG, "KsuService disconnected")
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) {
while (!isPlatformAlive) {
delay(500)
}
} ?: return@withContext // Exit early if timeout
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val binder = result.first val userInfos = Platform.userManager.getUsers()
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0) val packages = mutableListOf<PackageInfo>()
val packageManager = Platform.packageManager
withContext(Dispatchers.Main) { for (userInfo in userInfos) {
stopKsuService() Log.i(TAG, "fetchAppList: ${userInfo.id}")
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
} }
val packages = allPackages.list
apps = packages.map { apps = packages.map {
val appInfo = it.applicationInfo val appInfo = it.applicationInfo
val uid = appInfo!!.uid 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.WindowInsetsCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.sukisu.ultra.ui.util.createRootShell import com.sukisu.ultra.ui.util.createRootShell
import java.io.File import java.io.File
import com.dergoogler.mmrl.webui.interfaces.WXOptions
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() { class WebUIActivity : ComponentActivity() {
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
} else { } else {
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
setTaskDescription(taskDescription) setTaskDescription(taskDescription)
} }
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.domStorageEnabled = true settings.domStorageEnabled = true
settings.allowFileAccess = false settings.allowFileAccess = false
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir) webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
addJavascriptInterface(webviewInterface, "ksu") addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient) setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html") 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 package com.sukisu.ultra.ui.webui
import android.app.Activity import android.app.Activity
import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.TextUtils import android.text.TextUtils
import android.view.Window import android.view.Window
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat 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.CallbackList
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -25,10 +26,15 @@ import java.io.File
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class WebViewInterface( class WebViewInterface(
val context: Context, wxOptions: WXOptions,
private val webView: WebView, ) : WebUIInterface(wxOptions) {
private val modDir: String override var name: String = "ksu"
) {
companion object {
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
}
private val modDir get() = "/data/adb/modules/${modId.id}"
@JavascriptInterface @JavascriptInterface
fun exec(cmd: String): String { fun exec(cmd: String): String {
@@ -170,9 +176,9 @@ class WebViewInterface(
if (context is Activity) { if (context is Activity) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (enable) { if (enable) {
hideSystemUI(context.window) hideSystemUI(activity.window)
} else { } else {
showSystemUI(context.window) showSystemUI(activity.window)
} }
} }
} }

View File

@@ -226,7 +226,6 @@
<string name="batch_cancel_authorization">認証を一括でキャンセル</string> <string name="batch_cancel_authorization">認証を一括でキャンセル</string>
<string name="backup">バックアップ</string> <string name="backup">バックアップ</string>
<string name="color_yellow">イエロー</string> <string name="color_yellow">イエロー</string>
<string name="kpm">カーネルモジュール</string>
<string name="kpm_title">カーネルモジュール</string> <string name="kpm_title">カーネルモジュール</string>
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string> <string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
<string name="kpm_version">バージョン</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="batch_cancel_authorization">Hủy ủy quyền hàng loạt</string>
<string name="backup">Sao lưu</string> <string name="backup">Sao lưu</string>
<string name="color_yellow">Vàng</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_title">Mô-đun KPM</string>
<string name="kpm_empty">Không có mô-đun nào được cài đặt.</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> <string name="kpm_version">Phát hành</string>

View File

@@ -320,4 +320,26 @@
<string name="background_set_success">背景设置成功</string> <string name="background_set_success">背景设置成功</string>
<string name="background_removed">已移除自定义背景</string> <string name="background_removed">已移除自定义背景</string>
<string name="root_require_for_install">需要 root 权限</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> </resources>

View File

@@ -324,4 +324,26 @@
<string name="background_set_success">Background set successfully</string> <string name="background_set_success">Background set successfully</string>
<string name="background_removed">Removed custom backgrounds</string> <string name="background_removed">Removed custom backgrounds</string>
<string name="root_require_for_install">Requires root privileges</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> </resources>

View File

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

View File

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