Updated to 4.0.0

This commit is contained in:
Fr4nz D13trich 2025-11-20 21:24:53 +01:00
parent b7554a5383
commit 938198bf11
234 changed files with 21069 additions and 12710 deletions

View file

@ -2,7 +2,6 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.gradle.tasks.PackageAndroidArtifact
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.agp.app)
@ -17,6 +16,7 @@ plugins {
val managerVersionCode: Int by rootProject.extra
val managerVersionName: String by rootProject.extra
val androidCmakeVersion: String by rootProject.extra
apksign {
storeFileProperty = "KEYSTORE_FILE"
@ -51,15 +51,12 @@ android {
}
buildFeatures {
aidl = true
buildConfig = true
compose = true
prefab = true
}
kotlin {
jvmToolchain(21)
}
packaging {
jniLibs {
useLegacyPackaging = true
@ -77,7 +74,8 @@ android {
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
path = file("src/main/cpp/CMakeLists.txt")
version = androidCmakeVersion
}
}
@ -125,6 +123,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.foundation)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.compose.foundation)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)

View file

@ -43,4 +43,6 @@
-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 { *; }
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
-keep interface com.sukisu.zako.** { *; }

View file

@ -18,31 +18,82 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.KernelSU"
android:requestLegacyExternalStorage="true"
tools:targetApi="34">
<!-- 专门为小米手机桌面卸载添加了提示,提升用户体验 -->
<meta-data
android:name="app_description_title"
android:resource="@string/miui_uninstall_title" />
<meta-data
android:name="app_description_content"
android:resource="@string/miui_uninstall_content" />
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:enabled="true"
android:launchMode="standard"
android:documentLaunchMode="intoExisting"
android:autoRemoveFromRecents="true"
android:theme="@style/Theme.KernelSU">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
<data android:scheme="content" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
</activity>
<!-- 切换图标 -->
<activity-alias
android:name=".ui.MainActivityAlias"
android:targetActivity=".ui.MainActivity"
android:exported="true"
android:enabled="false"
android:icon="@mipmap/ic_launcher_alt"
android:roundIcon="@mipmap/ic_launcher_alt_round"
android:targetActivity=".ui.MainActivity">
android:roundIcon="@mipmap/ic_launcher_alt_round">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
</activity-alias>
<activity

View file

@ -0,0 +1,10 @@
// IKsuInterface.aidl
package com.sukisu.zako;
import android.content.pm.PackageInfo;
import java.util.List;
interface IKsuInterface {
int getPackageCount();
List<PackageInfo> getPackages(int start, int maxCount);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -6,10 +6,11 @@ cmake_minimum_required(VERSION 3.18.1)
project("kernelsu")
add_library(zako
add_library(kernelsu
SHARED
jni.c
ksu.c
legacy.c
)
find_library(log-lib log)
@ -21,7 +22,7 @@ elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
endif()
if(ANDROID_ABI STREQUAL "arm64-v8a" OR ANDROID_ABI STREQUAL "armeabi-v7a")
target_link_libraries(zako ${log-lib} ${zakosign-lib})
target_link_libraries(kernelsu ${log-lib} ${zakosign-lib})
else()
target_link_libraries(zako ${log-lib})
target_link_libraries(kernelsu ${log-lib})
endif()

View file

@ -5,430 +5,440 @@
#include <sys/prctl.h>
#include <android/log.h>
#include <string.h>
NativeBridge(becomeManager, jboolean, jstring pkg) {
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE);
bool result = become_manager(cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
return result;
}
#include <linux/capability.h>
#include <pwd.h>
NativeBridgeNP(getVersion, jint) {
return get_version();
uint32_t version = get_version();
if (version > 0) {
return (jint)version;
}
// try legacy method as fallback
return legacy_get_info().version;
}
// get VERSION FULL
NativeBridgeNP(getFullVersion, jstring) {
char buff[255] = { 0 };
get_full_version((char *) &buff);
return GetEnvironment()->NewStringUTF(env, buff);
char buff[255] = { 0 };
get_full_version((char *) &buff);
return GetEnvironment()->NewStringUTF(env, buff);
}
NativeBridgeNP(getAllowList, jintArray) {
int uids[1024];
int size = 0;
bool result = get_allow_list(uids, &size);
struct ksu_get_allow_list_cmd cmd = {};
bool result = get_allow_list(&cmd);
LogDebug("getAllowList: %d, size: %d", result, size);
if (result) {
jsize array_size = (jsize)cmd.count;
if (array_size < 0 || (unsigned int)array_size != cmd.count) {
LogDebug("Invalid array size: %u", cmd.count);
return GetEnvironment()->NewIntArray(env, 0);
}
if (result) {
jintArray array = GetEnvironment()->NewIntArray(env, size);
GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids);
jintArray array = GetEnvironment()->NewIntArray(env, array_size);
GetEnvironment()->SetIntArrayRegion(env, array, 0, array_size, (const jint *)(cmd.uids));
return array;
}
return array;
}
return GetEnvironment()->NewIntArray(env, 0);
return GetEnvironment()->NewIntArray(env, 0);
}
NativeBridgeNP(isSafeMode, jboolean) {
return is_safe_mode();
return is_safe_mode();
}
NativeBridgeNP(isLkmMode, jboolean) {
return is_lkm_mode();
return is_lkm_mode();
}
NativeBridgeNP(isManager, jboolean) {
return is_manager();
}
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
}
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
}
}
static void addIntToList(JNIEnv *env, jobject list, int ele) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
}
static uint64_t capListToBits(JNIEnv *env, jobject list) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
jint listSize = GetEnvironment()->CallIntMethod(env, list, size);
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
uint64_t result = 0;
for (int i = 0; i < listSize; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
int data = GetEnvironment()->CallIntMethod(env, integer, intValue);
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
jint listSize = GetEnvironment()->CallIntMethod(env, list, size);
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
uint64_t result = 0;
for (int i = 0; i < listSize; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
int data = GetEnvironment()->CallIntMethod(env, integer, intValue);
if (cap_valid(data)) {
result |= (1ULL << data);
}
}
if (cap_valid(data)) {
result |= (1ULL << data);
}
}
return result;
return result;
}
static int getListSize(JNIEnv *env, jobject list) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
return GetEnvironment()->CallIntMethod(env, list, size);
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
return GetEnvironment()->CallIntMethod(env, list, size);
}
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue);
}
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue);
}
}
NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) {
if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) {
return NULL;
}
if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) {
return NULL;
}
char key[KSU_MAX_PACKAGE_NAME] = { 0 };
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr);
strcpy(key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
char key[KSU_MAX_PACKAGE_NAME] = { 0 };
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr);
strcpy(key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
struct app_profile profile = { 0 };
profile.version = KSU_APP_PROFILE_VER;
struct app_profile profile = { 0 };
profile.version = KSU_APP_PROFILE_VER;
strcpy(profile.key, key);
profile.current_uid = uid;
strcpy(profile.key, key);
profile.current_uid = uid;
bool useDefaultProfile = !get_app_profile(key, &profile);
bool useDefaultProfile = get_app_profile(&profile) != 0;
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key));
GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid);
GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key));
GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid);
if (useDefaultProfile) {
// no profile found, so just use default profile:
// don't allow root and use default profile!
LogDebug("use default profile for: %s, %d", key, uid);
if (useDefaultProfile) {
// no profile found, so just use default profile:
// don't allow root and use default profile!
LogDebug("use default profile for: %s, %d", key, uid);
// allow_su = false
// non root use default = true
GetEnvironment()->SetBooleanField(env, obj, allowSuField, false);
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true);
// allow_su = false
// non root use default = true
GetEnvironment()->SetBooleanField(env, obj, allowSuField, false);
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true);
return obj;
}
return obj;
}
bool allowSu = profile.allow_su;
bool allowSu = profile.allow_su;
if (allowSu) {
GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
if (strlen(profile.rp_config.template_name) > 0) {
GetEnvironment()->SetObjectField(env, obj, rootTemplateField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name));
}
if (allowSu) {
GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
if (strlen(profile.rp_config.template_name) > 0) {
GetEnvironment()->SetObjectField(env, obj, rootTemplateField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name));
}
GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid);
GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid);
GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid);
GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid);
jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField);
int groupCount = profile.rp_config.profile.groups_count;
if (groupCount > KSU_MAX_GROUPS) {
LogDebug("kernel group count too large: %d???", groupCount);
groupCount = KSU_MAX_GROUPS;
}
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField);
int groupCount = profile.rp_config.profile.groups_count;
if (groupCount > KSU_MAX_GROUPS) {
LogDebug("kernel group count too large: %d???", groupCount);
groupCount = KSU_MAX_GROUPS;
}
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField);
for (int i = 0; i <= CAP_LAST_CAP; i++) {
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
addIntToList(env, capList, i);
}
}
jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField);
for (int i = 0; i <= CAP_LAST_CAP; i++) {
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
addIntToList(env, capList, i);
}
}
GetEnvironment()->SetObjectField(env, obj, domainField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain));
GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces);
GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su);
} else {
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default);
GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules);
}
GetEnvironment()->SetObjectField(env, obj, domainField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain));
GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces);
GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su);
} else {
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default);
GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules);
}
return obj;
return obj;
}
NativeBridge(setAppProfile, jboolean, jobject profile) {
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
jobject key = GetEnvironment()->GetObjectField(env, profile, keyField);
if (!key) {
return false;
}
if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) {
return false;
}
jobject key = GetEnvironment()->GetObjectField(env, profile, keyField);
if (!key) {
return false;
}
if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) {
return false;
}
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr);
char p_key[KSU_MAX_PACKAGE_NAME] = { 0 };
strcpy(p_key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg);
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr);
char p_key[KSU_MAX_PACKAGE_NAME] = { 0 };
strcpy(p_key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg);
jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField);
jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField);
jint uid = GetEnvironment()->GetIntField(env, profile, uidField);
jint gid = GetEnvironment()->GetIntField(env, profile, gidField);
jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField);
jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField);
jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField);
jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField);
jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField);
jint uid = GetEnvironment()->GetIntField(env, profile, uidField);
jint gid = GetEnvironment()->GetIntField(env, profile, gidField);
jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField);
jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField);
jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField);
jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField);
jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField);
struct app_profile p = { 0 };
p.version = KSU_APP_PROFILE_VER;
struct app_profile p = { 0 };
p.version = KSU_APP_PROFILE_VER;
strcpy(p.key, p_key);
p.allow_su = allowSu;
p.current_uid = currentUid;
strcpy(p.key, p_key);
p.allow_su = allowSu;
p.current_uid = currentUid;
if (allowSu) {
p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField);
jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField);
if (templateName) {
const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr);
strcpy(p.rp_config.template_name, ctemplateName);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName);
}
if (allowSu) {
p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField);
jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField);
if (templateName) {
const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr);
strcpy(p.rp_config.template_name, ctemplateName);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName);
}
p.rp_config.profile.uid = uid;
p.rp_config.profile.gid = gid;
p.rp_config.profile.uid = uid;
p.rp_config.profile.gid = gid;
int groups_count = getListSize(env, groups);
if (groups_count > KSU_MAX_GROUPS) {
LogDebug("groups count too large: %d", groups_count);
return false;
}
p.rp_config.profile.groups_count = groups_count;
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
int groups_count = getListSize(env, groups);
if (groups_count > KSU_MAX_GROUPS) {
LogDebug("groups count too large: %d", groups_count);
return false;
}
p.rp_config.profile.groups_count = groups_count;
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr);
strcpy(p.rp_config.profile.selinux_domain, cdomain);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain);
const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr);
strcpy(p.rp_config.profile.selinux_domain, cdomain);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain);
p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField);
} else {
p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField);
p.nrp_config.profile.umount_modules = umountModules;
}
p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField);
} else {
p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField);
p.nrp_config.profile.umount_modules = umountModules;
}
return set_app_profile(&p);
return set_app_profile(&p);
}
NativeBridge(uidShouldUmount, jboolean, jint uid) {
return uid_should_umount(uid);
return uid_should_umount(uid);
}
NativeBridgeNP(isSuEnabled, jboolean) {
return is_su_enabled();
return is_su_enabled();
}
NativeBridge(setSuEnabled, jboolean, jboolean enabled) {
return set_su_enabled(enabled);
return set_su_enabled(enabled);
}
NativeBridgeNP(isKernelUmountEnabled, jboolean) {
return is_kernel_umount_enabled();
}
NativeBridge(setKernelUmountEnabled, jboolean, jboolean enabled) {
return set_kernel_umount_enabled(enabled);
}
NativeBridgeNP(isEnhancedSecurityEnabled, jboolean) {
return is_enhanced_security_enabled();
}
NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean enabled) {
return set_enhanced_security_enabled(enabled);
}
NativeBridge(getUserName, jstring, jint uid) {
struct passwd *pw = getpwuid((uid_t) uid);
if (pw && pw->pw_name && pw->pw_name[0] != '\0') {
return GetEnvironment()->NewStringUTF(env, pw->pw_name);
}
return NULL;
}
// Check if KPM is enabled
NativeBridgeNP(isKPMEnabled, jboolean) {
return is_KPM_enable();
return is_KPM_enable();
}
// Get HOOK type
NativeBridgeNP(getHookType, jstring) {
char hook_type[16];
get_hook_type(hook_type, sizeof(hook_type));
return GetEnvironment()->NewStringUTF(env, hook_type);
}
// SuSFS Related Function Status
NativeBridgeNP(getSusfsFeatureStatus, jobject) {
struct susfs_feature_status status;
bool result = get_susfs_feature_status(&status);
if (!result) {
return NULL;
}
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus");
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
SET_BOOLEAN_FIELD(obj, cls, statusSusPath, status.status_sus_path);
SET_BOOLEAN_FIELD(obj, cls, statusSusMount, status.status_sus_mount);
SET_BOOLEAN_FIELD(obj, cls, statusAutoDefaultMount, status.status_auto_default_mount);
SET_BOOLEAN_FIELD(obj, cls, statusAutoBindMount, status.status_auto_bind_mount);
SET_BOOLEAN_FIELD(obj, cls, statusSusKstat, status.status_sus_kstat);
SET_BOOLEAN_FIELD(obj, cls, statusTryUmount, status.status_try_umount);
SET_BOOLEAN_FIELD(obj, cls, statusAutoTryUmountBind, status.status_auto_try_umount_bind);
SET_BOOLEAN_FIELD(obj, cls, statusSpoofUname, status.status_spoof_uname);
SET_BOOLEAN_FIELD(obj, cls, statusEnableLog, status.status_enable_log);
SET_BOOLEAN_FIELD(obj, cls, statusHideSymbols, status.status_hide_symbols);
SET_BOOLEAN_FIELD(obj, cls, statusSpoofCmdline, status.status_spoof_cmdline);
SET_BOOLEAN_FIELD(obj, cls, statusOpenRedirect, status.status_open_redirect);
SET_BOOLEAN_FIELD(obj, cls, statusMagicMount, status.status_magic_mount);
SET_BOOLEAN_FIELD(obj, cls, statusSusSu, status.status_sus_su);
return obj;
char hook_type[32] = { 0 };
get_hook_type((char *) &hook_type);
return GetEnvironment()->NewStringUTF(env, hook_type);
}
// dynamic manager
NativeBridge(setDynamicManager, jboolean, jint size, jstring hash) {
if (!hash) {
LogDebug("setDynamicManager: hash is null");
return false;
}
if (!hash) {
LogDebug("setDynamicManager: hash is null");
return false;
}
const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr);
bool result = set_dynamic_manager((unsigned int)size, chash);
GetEnvironment()->ReleaseStringUTFChars(env, hash, chash);
const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr);
bool result = set_dynamic_manager((unsigned int)size, chash);
GetEnvironment()->ReleaseStringUTFChars(env, hash, chash);
LogDebug("setDynamicManager: size=0x%x, result=%d", size, result);
return result;
LogDebug("setDynamicManager: size=0x%x, result=%d", size, result);
return result;
}
NativeBridgeNP(getDynamicManager, jobject) {
struct dynamic_manager_user_config config;
bool result = get_dynamic_manager(&config);
struct dynamic_manager_user_config config;
bool result = get_dynamic_manager(&config);
if (!result) {
LogDebug("getDynamicManager: failed to get dynamic manager config");
return NULL;
}
if (!result) {
LogDebug("getDynamicManager: failed to get dynamic manager config");
return NULL;
}
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig");
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig");
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig");
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig");
SET_INT_FIELD(obj, cls, size, (jint)config.size);
SET_STRING_FIELD(obj, cls, hash, config.hash);
SET_INT_FIELD(obj, cls, size, (jint)config.size);
SET_STRING_FIELD(obj, cls, hash, config.hash);
LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash);
return obj;
LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash);
return obj;
}
NativeBridgeNP(clearDynamicManager, jboolean) {
bool result = clear_dynamic_manager();
LogDebug("clearDynamicManager: result=%d", result);
return result;
bool result = clear_dynamic_manager();
LogDebug("clearDynamicManager: result=%d", result);
return result;
}
// Get a list of active managers
NativeBridgeNP(getManagersList, jobject) {
struct manager_list_info managerListInfo;
bool result = get_managers_list(&managerListInfo);
struct manager_list_info managerListInfo;
bool result = get_managers_list(&managerListInfo);
if (!result) {
LogDebug("getManagersList: failed to get active managers list");
return NULL;
}
if (!result) {
LogDebug("getManagersList: failed to get active managers list");
return NULL;
}
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList");
jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList");
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList");
jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList");
SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count);
SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count);
jobject managersList = CREATE_ARRAYLIST();
jobject managersList = CREATE_ARRAYLIST();
for (int i = 0; i < managerListInfo.count; i++) {
jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS(
"com/sukisu/ultra/Natives$ManagerInfo",
"(II)V",
(jint)managerListInfo.managers[i].uid,
(jint)managerListInfo.managers[i].signature_index
);
ADD_TO_LIST(managersList, managerInfo);
}
for (int i = 0; i < managerListInfo.count; i++) {
jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS(
"com/sukisu/ultra/Natives$ManagerInfo",
"(II)V",
(jint)managerListInfo.managers[i].uid,
(jint)managerListInfo.managers[i].signature_index
);
ADD_TO_LIST(managersList, managerInfo);
}
SET_OBJECT_FIELD(obj, managerListCls, managers, managersList);
SET_OBJECT_FIELD(obj, managerListCls, managers, managersList);
LogDebug("getManagersList: count=%d", managerListInfo.count);
return obj;
LogDebug("getManagersList: count=%d", managerListInfo.count);
return obj;
}
NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) {
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
if (!modulePath) {
LogDebug("verifyModuleSignature: modulePath is null");
return false;
}
if (!modulePath) {
LogDebug("verifyModuleSignature: modulePath is null");
return false;
}
const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr);
bool result = verify_module_signature(cModulePath);
GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath);
const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr);
bool result = verify_module_signature(cModulePath);
GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath);
LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result);
return result;
LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result);
return result;
#else
LogDebug("verifyModuleSignature: not supported on non-ARM architecture");
return false;
LogDebug("verifyModuleSignature: not supported on non-ARM architecture");
return false;
#endif
}
NativeBridgeNP(isUidScannerEnabled, jboolean) {
return is_uid_scanner_enabled();
}
NativeBridge(setUidScannerEnabled, jboolean, jboolean enabled) {
return set_uid_scanner_enabled(enabled);
}
NativeBridgeNP(clearUidScannerEnvironment, jboolean) {
return clear_uid_scanner_environment();
}

View file

@ -2,11 +2,14 @@
// Created by weishu on 2022/12/9.
//
#include <sys/prctl.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <android/log.h>
#include <dirent.h>
#include <stdlib.h>
#include <limits.h>
#include "prelude.h"
#include "ksu.h"
@ -21,232 +24,367 @@ extern const char* zako_file_verrcidx2str(uint8_t index);
#endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM
#define KERNEL_SU_OPTION 0xDEADBEEF
static int fd = -1;
#define CMD_GRANT_ROOT 0
static inline int scan_driver_fd() {
const char *kName = "[ksu_driver]";
DIR *fd_dir = opendir("/proc/self/fd");
if (!fd_dir) {
return -1;
}
#define CMD_BECOME_MANAGER 1
#define CMD_GET_VERSION 2
#define CMD_ALLOW_SU 3
#define CMD_DENY_SU 4
#define CMD_GET_SU_LIST 5
#define CMD_GET_DENY_LIST 6
#define CMD_CHECK_SAFEMODE 9
int found = -1;
struct dirent *de;
char path[64];
char target[PATH_MAX];
#define CMD_GET_APP_PROFILE 10
#define CMD_SET_APP_PROFILE 11
while ((de = readdir(fd_dir)) != NULL) {
if (de->d_name[0] == '.') {
continue;
}
#define CMD_IS_UID_GRANTED_ROOT 12
#define CMD_IS_UID_SHOULD_UMOUNT 13
#define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15
char *endptr = nullptr;
long fd_long = strtol(de->d_name, &endptr, 10);
if (!de->d_name[0] || *endptr != '\0' || fd_long < 0 || fd_long > INT_MAX) {
continue;
}
#define CMD_GET_VERSION_FULL 0xC0FFEE1A
snprintf(path, sizeof(path), "/proc/self/fd/%s", de->d_name);
ssize_t n = readlink(path, target, sizeof(target) - 1);
if (n < 0) {
continue;
}
target[n] = '\0';
#define CMD_ENABLE_KPM 100
#define CMD_HOOK_TYPE 101
#define CMD_GET_SUSFS_FEATURE_STATUS 102
#define CMD_DYNAMIC_MANAGER 103
#define CMD_GET_MANAGERS 104
const char *base = strrchr(target, '/');
base = base ? base + 1 : target;
#define DYNAMIC_MANAGER_OP_SET 0
#define DYNAMIC_MANAGER_OP_GET 1
#define DYNAMIC_MANAGER_OP_CLEAR 2
if (strstr(base, kName)) {
found = (int)fd_long;
break;
}
}
static bool ksuctl(int cmd, void* arg1, void* arg2) {
int32_t result = 0;
int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
return result == KERNEL_SU_OPTION && rtn == -1;
closedir(fd_dir);
return found;
}
bool become_manager(const char* pkg) {
char param[128];
uid_t uid = getuid();
uint32_t userId = uid / 100000;
if (userId == 0) {
sprintf(param, "/data/data/%s", pkg);
} else {
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
static int ksuctl(unsigned long op, void* arg) {
if (fd < 0) {
fd = scan_driver_fd();
}
return ioctl(fd, op, arg);
}
static struct ksu_get_info_cmd g_version = {0};
struct ksu_get_info_cmd get_info() {
if (!g_version.version) {
ksuctl(KSU_IOCTL_GET_INFO, &g_version);
}
return g_version;
}
uint32_t get_version() {
auto info = get_info();
return info.version;
}
bool get_allow_list(struct ksu_get_allow_list_cmd *cmd) {
if (ksuctl(KSU_IOCTL_GET_ALLOW_LIST, cmd) == 0) {
return true;
}
return ksuctl(CMD_BECOME_MANAGER, param, NULL);
}
// cache the result to avoid unnecessary syscall
static bool is_lkm;
int get_version() {
int32_t version = -1;
int32_t flags = 0;
ksuctl(CMD_GET_VERSION, &version, &flags);
if (!is_lkm && (flags & 0x1)) {
is_lkm = true;
// fallback to legacy
int size = 0;
int uids[1024];
if (legacy_get_allow_list(uids, &size)) {
cmd->count = size;
memcpy(cmd->uids, uids, sizeof(int) * size);
return true;
}
return version;
}
void get_full_version(char* buff) {
ksuctl(CMD_GET_VERSION_FULL, buff, NULL);
}
bool get_allow_list(int *uids, int *size) {
return ksuctl(CMD_GET_SU_LIST, uids, size);
return false;
}
bool is_safe_mode() {
return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL);
struct ksu_check_safemode_cmd cmd = {};
if (ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &cmd) == 0) {
return cmd.in_safe_mode;
}
// fallback
return legacy_is_safe_mode();
}
bool is_lkm_mode() {
// you should call get_version first!
return is_lkm;
auto info = get_info();
if (info.version > 0) {
return (info.flags & 0x1) != 0;
}
// Legacy Compatible
return (legacy_get_info().flags & 0x1) != 0;
}
bool is_manager() {
auto info = get_info();
if (info.version > 0) {
return (info.flags & 0x2) != 0;
}
// Legacy Compatible
return legacy_get_info().version > 0;
}
bool uid_should_umount(int uid) {
int should;
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should;
struct ksu_uid_should_umount_cmd cmd = {};
cmd.uid = uid;
if (ksuctl(KSU_IOCTL_UID_SHOULD_UMOUNT, &cmd) == 0) {
return cmd.should_umount;
}
return legacy_uid_should_umount(uid);
}
bool set_app_profile(const struct app_profile* profile) {
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL);
bool set_app_profile(const struct app_profile *profile) {
struct ksu_set_app_profile_cmd cmd = {};
cmd.profile = *profile;
if (ksuctl(KSU_IOCTL_SET_APP_PROFILE, &cmd) == 0) {
return true;
}
return legacy_set_app_profile(profile);
}
bool get_app_profile(char* key, struct app_profile* profile) {
return ksuctl(CMD_GET_APP_PROFILE, profile, NULL);
int get_app_profile(struct app_profile *profile) {
struct ksu_get_app_profile_cmd cmd = {.profile = *profile};
int ret = ksuctl(KSU_IOCTL_GET_APP_PROFILE, &cmd);
if (ret == 0) {
*profile = cmd.profile;
return 0;
}
return legacy_get_app_profile(profile->key, profile) ? 0 : -1;
}
bool set_su_enabled(bool enabled) {
return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL);
struct ksu_set_feature_cmd cmd = {};
cmd.feature_id = KSU_FEATURE_SU_COMPAT;
cmd.value = enabled ? 1 : 0;
if (ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0) {
return true;
}
return legacy_set_su_enabled(enabled);
}
bool is_su_enabled() {
int enabled = true;
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL);
return enabled;
struct ksu_get_feature_cmd cmd = {};
cmd.feature_id = KSU_FEATURE_SU_COMPAT;
if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) == 0 && cmd.supported) {
return cmd.value != 0;
}
return legacy_is_su_enabled();
}
bool is_KPM_enable() {
int enabled = false;
ksuctl(CMD_ENABLE_KPM, &enabled, NULL);
return enabled;
}
bool get_hook_type(char* hook_type, size_t size) {
if (hook_type == NULL || size == 0) {
static inline bool get_feature(uint32_t feature_id, uint64_t *out_value, bool *out_supported) {
struct ksu_get_feature_cmd cmd = {};
cmd.feature_id = feature_id;
if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) {
return false;
}
static char cached_hook_type[16] = {0};
if (cached_hook_type[0] == '\0') {
if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) {
strcpy(cached_hook_type, "Unknown");
}
}
strncpy(hook_type, cached_hook_type, size);
hook_type[size - 1] = '\0';
if (out_value) *out_value = cmd.value;
if (out_supported) *out_supported = cmd.supported;
return true;
}
bool get_susfs_feature_status(struct susfs_feature_status* status) {
if (status == NULL) {
return false;
}
return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL);
static inline bool set_feature(uint32_t feature_id, uint64_t value) {
struct ksu_set_feature_cmd cmd = {};
cmd.feature_id = feature_id;
cmd.value = value;
return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0;
}
bool set_dynamic_manager(unsigned int size, const char* hash) {
if (hash == NULL) {
return false;
}
struct dynamic_manager_user_config config;
config.operation = DYNAMIC_MANAGER_OP_SET;
config.size = size;
strncpy(config.hash, hash, sizeof(config.hash) - 1);
config.hash[sizeof(config.hash) - 1] = '\0';
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
bool set_kernel_umount_enabled(bool enabled) {
return set_feature(KSU_FEATURE_KERNEL_UMOUNT, enabled ? 1 : 0);
}
bool get_dynamic_manager(struct dynamic_manager_user_config* config) {
if (config == NULL) {
bool is_kernel_umount_enabled() {
uint64_t value = 0;
bool supported = false;
if (!get_feature(KSU_FEATURE_KERNEL_UMOUNT, &value, &supported)) {
return false;
}
config->operation = DYNAMIC_MANAGER_OP_GET;
return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL);
}
bool clear_dynamic_manager() {
struct dynamic_manager_user_config config;
config.operation = DYNAMIC_MANAGER_OP_CLEAR;
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
}
bool get_managers_list(struct manager_list_info* info) {
if (info == NULL) {
if (!supported) {
return false;
}
return value != 0;
}
return ksuctl(CMD_GET_MANAGERS, info, NULL);
bool set_enhanced_security_enabled(bool enabled) {
return set_feature(KSU_FEATURE_ENHANCED_SECURITY, enabled ? 1 : 0);
}
bool is_enhanced_security_enabled() {
uint64_t value = 0;
bool supported = false;
if (!get_feature(KSU_FEATURE_ENHANCED_SECURITY, &value, &supported)) {
return false;
}
if (!supported) {
return false;
}
return value != 0;
}
void get_full_version(char* buff) {
struct ksu_get_full_version_cmd cmd = {0};
if (ksuctl(KSU_IOCTL_GET_FULL_VERSION, &cmd) == 0) {
strncpy(buff, cmd.version_full, KSU_FULL_VERSION_STRING - 1);
buff[KSU_FULL_VERSION_STRING - 1] = '\0';
} else {
return legacy_get_full_version(buff);
}
}
bool is_KPM_enable(void) {
struct ksu_enable_kpm_cmd cmd = {};
if (ksuctl(KSU_IOCTL_ENABLE_KPM, &cmd) == 0 && cmd.enabled) {
return true;
}
return legacy_is_KPM_enable();
}
void get_hook_type(char *buff) {
struct ksu_hook_type_cmd cmd = {0};
if (ksuctl(KSU_IOCTL_HOOK_TYPE, &cmd) == 0) {
strncpy(buff, cmd.hook_type, 32 - 1);
buff[32 - 1] = '\0';
} else {
legacy_get_hook_type(buff, 32);
}
}
bool set_dynamic_manager(unsigned int size, const char *hash)
{
struct ksu_dynamic_manager_cmd cmd = {0};
cmd.config.operation = DYNAMIC_MANAGER_OP_SET;
cmd.config.size = size;
strlcpy(cmd.config.hash, hash, sizeof(cmd.config.hash));
return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0;
}
bool get_dynamic_manager(struct dynamic_manager_user_config *cfg)
{
if (!cfg)
return false;
struct ksu_dynamic_manager_cmd cmd = {0};
cmd.config.operation = DYNAMIC_MANAGER_OP_GET;
if (ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) != 0)
return false;
*cfg = cmd.config;
return true;
}
bool clear_dynamic_manager(void)
{
struct ksu_dynamic_manager_cmd cmd = {0};
cmd.config.operation = DYNAMIC_MANAGER_OP_CLEAR;
return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0;
}
bool get_managers_list(struct manager_list_info *info)
{
if (!info)
return false;
struct ksu_get_managers_cmd cmd = {0};
if (ksuctl(KSU_IOCTL_GET_MANAGERS, &cmd) != 0)
return false;
*info = cmd.manager_info;
return true;
}
bool is_uid_scanner_enabled(void)
{
bool status = false;
struct ksu_enable_uid_scanner_cmd cmd = {
.operation = UID_SCANNER_OP_GET_STATUS,
.status_ptr = (__u64)(uintptr_t)&status
};
return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd) == 0 != 0 && status;
}
bool set_uid_scanner_enabled(bool enabled)
{
struct ksu_enable_uid_scanner_cmd cmd = {
.operation = UID_SCANNER_OP_TOGGLE,
.enabled = enabled
};
return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd);
}
bool clear_uid_scanner_environment(void)
{
struct ksu_enable_uid_scanner_cmd cmd = {
.operation = UID_SCANNER_OP_CLEAR_ENV
};
return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd);
}
bool verify_module_signature(const char* input) {
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
if (input == NULL) {
LogDebug("verify_module_signature: input path is null");
return false;
}
if (input == NULL) {
LogDebug("verify_module_signature: input path is null");
return false;
}
int fd = zako_sys_file_open(input);
if (fd < 0) {
LogDebug("verify_module_signature: failed to open file: %s", input);
return false;
}
int file_fd = zako_sys_file_open(input);
if (file_fd < 0) {
LogDebug("verify_module_signature: failed to open file: %s", input);
return false;
}
uint32_t results = zako_file_verify_esig(fd, 0);
uint32_t results = zako_file_verify_esig(file_fd, 0);
if (results != 0) {
/* If important error occured, verification process should
be considered as failed due to unexpected modification
potentially happened. */
if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) {
LogDebug("verify_module_signature: Verification failed! (important error)");
} else {
/* This is for manager that doesn't want to do certificate checks */
LogDebug("verify_module_signature: Verification partially passed");
}
} else {
LogDebug("verify_module_signature: Verification passed!");
goto exit;
}
if (results != 0) {
/* If important error occured, verification process should
be considered as failed due to unexpected modification
potentially happened. */
if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) {
LogDebug("verify_module_signature: Verification failed! (important error)");
} else {
/* This is for manager that doesn't want to do certificate checks */
LogDebug("verify_module_signature: Verification partially passed");
}
} else {
LogDebug("verify_module_signature: Verification passed!");
goto exit;
}
/* Go through all bit fields */
for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) {
if ((results & (1 << i)) == 0) {
continue;
}
/* Go through all bit fields */
for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) {
if ((results & (1 << i)) == 0) {
continue;
}
/* Convert error bit field index into human readable string */
const char* message = zako_file_verrcidx2str((uint8_t)i);
// Error message: message
if (message != NULL) {
LogDebug("verify_module_signature: Error bit %zu: %s", i, message);
} else {
LogDebug("verify_module_signature: Error bit %zu: Unknown error", i);
}
}
/* Convert error bit field index into human readable string */
const char* message = zako_file_verrcidx2str((uint8_t)i);
// Error message: message
if (message != NULL) {
LogDebug("verify_module_signature: Error bit %zu: %s", i, message);
} else {
LogDebug("verify_module_signature: Error bit %zu: Unknown error", i);
}
}
exit:
close(fd);
LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s",
input, results, (results == 0) ? "true" : "false");
return results == 0;
exit:
close(file_fd);
LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s",
input, results, (results == 0) ? "true" : "false");
return results == 0;
#else
LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null");
return false;
LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null");
return false;
#endif
}
}

View file

@ -6,16 +6,15 @@
#define KERNELSU_KSU_H
#include "prelude.h"
#include <linux/capability.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
bool become_manager(const char *);
#define KSU_FULL_VERSION_STRING 255
void get_full_version(char* buff);
int get_version();
bool get_allow_list(int *uids, int *size);
uint32_t get_version();
bool uid_should_umount(int uid);
@ -23,6 +22,10 @@ bool is_safe_mode();
bool is_lkm_mode();
bool is_manager();
void get_full_version(char* buff);
#define KSU_APP_PROFILE_VER 2
#define KSU_MAX_PACKAGE_NAME 256
// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups.
@ -33,99 +36,80 @@ bool is_lkm_mode();
#define DYNAMIC_MANAGER_OP_GET 1
#define DYNAMIC_MANAGER_OP_CLEAR 2
#define UID_SCANNER_OP_GET_STATUS 0
#define UID_SCANNER_OP_TOGGLE 1
#define UID_SCANNER_OP_CLEAR_ENV 2
struct dynamic_manager_user_config {
unsigned int operation;
unsigned int size;
char hash[65];
unsigned int operation;
unsigned int size;
char hash[65];
};
// SUSFS Functional State Structures
struct susfs_feature_status {
bool status_sus_path;
bool status_sus_mount;
bool status_auto_default_mount;
bool status_auto_bind_mount;
bool status_sus_kstat;
bool status_try_umount;
bool status_auto_try_umount_bind;
bool status_spoof_uname;
bool status_enable_log;
bool status_hide_symbols;
bool status_spoof_cmdline;
bool status_open_redirect;
bool status_magic_mount;
bool status_sus_su;
};
struct root_profile {
int32_t uid;
int32_t gid;
int32_t uid;
int32_t gid;
int32_t groups_count;
int32_t groups[KSU_MAX_GROUPS];
int32_t groups_count;
int32_t groups[KSU_MAX_GROUPS];
// kernel_cap_t is u32[2] for capabilities v3
struct {
uint64_t effective;
uint64_t permitted;
uint64_t inheritable;
} capabilities;
// kernel_cap_t is u32[2] for capabilities v3
struct {
uint64_t effective;
uint64_t permitted;
uint64_t inheritable;
} capabilities;
char selinux_domain[KSU_SELINUX_DOMAIN];
char selinux_domain[KSU_SELINUX_DOMAIN];
int32_t namespaces;
int32_t namespaces;
};
struct non_root_profile {
bool umount_modules;
bool umount_modules;
};
struct app_profile {
// It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.
uint32_t version;
// It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.
uint32_t version;
// this is usually the package of the app, but can be other value for special apps
char key[KSU_MAX_PACKAGE_NAME];
int32_t current_uid;
bool allow_su;
// this is usually the package of the app, but can be other value for special apps
char key[KSU_MAX_PACKAGE_NAME];
int32_t current_uid;
bool allow_su;
union {
struct {
bool use_default;
char template_name[KSU_MAX_PACKAGE_NAME];
union {
struct {
bool use_default;
char template_name[KSU_MAX_PACKAGE_NAME];
struct root_profile profile;
} rp_config;
struct root_profile profile;
} rp_config;
struct {
bool use_default;
struct {
bool use_default;
struct non_root_profile profile;
} nrp_config;
};
struct non_root_profile profile;
} nrp_config;
};
};
struct manager_list_info {
int count;
struct {
uid_t uid;
int signature_index;
} managers[2];
int count;
struct {
uid_t uid;
int signature_index;
} managers[2];
};
bool set_app_profile(const struct app_profile* profile);
bool get_app_profile(char* key, struct app_profile* profile);
bool set_su_enabled(bool enabled);
bool is_su_enabled();
int get_app_profile(struct app_profile* profile);
bool is_KPM_enable();
bool get_hook_type(char* hook_type, size_t size);
bool get_susfs_feature_status(struct susfs_feature_status* status);
void get_hook_type(char* hook_type);
bool set_dynamic_manager(unsigned int size, const char* hash);
@ -137,4 +121,176 @@ bool get_managers_list(struct manager_list_info* info);
bool verify_module_signature(const char* input);
bool is_uid_scanner_enabled();
bool set_uid_scanner_enabled(bool enabled);
bool clear_uid_scanner_environment();
// Feature IDs
enum ksu_feature_id {
KSU_FEATURE_SU_COMPAT = 0,
KSU_FEATURE_KERNEL_UMOUNT = 1,
KSU_FEATURE_ENHANCED_SECURITY = 2,
};
// Generic feature API
struct ksu_get_feature_cmd {
uint32_t feature_id; // Input: feature ID
uint64_t value; // Output: feature value/state
uint8_t supported; // Output: whether the feature is supported
};
struct ksu_set_feature_cmd {
uint32_t feature_id; // Input: feature ID
uint64_t value; // Input: feature value/state to set
};
struct ksu_become_daemon_cmd {
uint8_t token[65]; // Input: daemon token (null-terminated)
};
struct ksu_get_info_cmd {
uint32_t version; // Output: KERNEL_SU_VERSION
uint32_t flags; // Output: flags (bit 0: MODULE mode)
uint32_t features; // Output: max feature ID supported (KSU_FEATURE_MAX)
};
struct ksu_report_event_cmd {
uint32_t event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc.
};
struct ksu_set_sepolicy_cmd {
uint64_t cmd; // Input: sepolicy command
uint64_t arg; // Input: sepolicy argument pointer
};
struct ksu_check_safemode_cmd {
uint8_t in_safe_mode; // Output: true if in safe mode, false otherwise
};
struct ksu_get_allow_list_cmd {
uint32_t uids[128]; // Output: array of allowed/denied UIDs
uint32_t count; // Output: number of UIDs in array
uint8_t allow; // Input: true for allow list, false for deny list
};
struct ksu_uid_granted_root_cmd {
uint32_t uid; // Input: target UID to check
uint8_t granted; // Output: true if granted, false otherwise
};
struct ksu_uid_should_umount_cmd {
uint32_t uid; // Input: target UID to check
uint8_t should_umount; // Output: true if should umount, false otherwise
};
struct ksu_get_manager_uid_cmd {
uint32_t uid; // Output: manager UID
};
struct ksu_set_manager_uid_cmd {
uint32_t uid; // Input: new manager UID
};
struct ksu_get_app_profile_cmd {
struct app_profile profile; // Input/Output: app profile structure
};
struct ksu_set_app_profile_cmd {
struct app_profile profile; // Input: app profile structure
};
// Su compat
bool set_su_enabled(bool enabled);
bool is_su_enabled();
// Kernel umount
bool set_kernel_umount_enabled(bool enabled);
bool is_kernel_umount_enabled();
// Enhanced security
bool set_enhanced_security_enabled(bool enabled);
bool is_enhanced_security_enabled();
// Other command structures
struct ksu_get_full_version_cmd {
char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string
};
struct ksu_hook_type_cmd {
char hook_type[32]; // Output: hook type string
};
struct ksu_enable_kpm_cmd {
uint8_t enabled; // Output: true if KPM is enabled
};
struct ksu_dynamic_manager_cmd {
struct dynamic_manager_user_config config; // Input/Output: dynamic manager config
};
struct ksu_get_managers_cmd {
struct manager_list_info manager_info; // Output: manager list information
};
struct ksu_enable_uid_scanner_cmd {
uint32_t operation; // Input: operation type (UID_SCANNER_OP_GET_STATUS, UID_SCANNER_OP_TOGGLE, UID_SCANNER_OP_CLEAR_ENV)
uint32_t enabled; // Input: enable or disable (for UID_SCANNER_OP_TOGGLE)
uint64_t status_ptr; // Input: pointer to store status (for UID_SCANNER_OP_GET_STATUS)
};
// IOCTL command definitions
#define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0)
#define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0)
#define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0)
#define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0)
#define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0)
#define KSU_IOCTL_GET_ALLOW_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 6, 0)
#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 7, 0)
#define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ|_IOC_WRITE, 'K', 8, 0)
#define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ|_IOC_WRITE, 'K', 9, 0)
#define KSU_IOCTL_GET_MANAGER_UID _IOC(_IOC_READ, 'K', 10, 0)
#define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ|_IOC_WRITE, 'K', 11, 0)
#define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0)
#define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0)
#define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0)
// Other IOCTL command definitions
#define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0)
#define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0)
#define KSU_IOCTL_ENABLE_KPM _IOC(_IOC_READ, 'K', 102, 0)
#define KSU_IOCTL_DYNAMIC_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 103, 0)
#define KSU_IOCTL_GET_MANAGERS _IOC(_IOC_READ|_IOC_WRITE, 'K', 104, 0)
#define KSU_IOCTL_ENABLE_UID_SCANNER _IOC(_IOC_READ|_IOC_WRITE, 'K', 105, 0)
bool get_allow_list(struct ksu_get_allow_list_cmd *);
// Legacy Compatible
struct ksu_version_info legacy_get_info();
struct ksu_version_info {
int32_t version;
int32_t flags;
};
bool legacy_get_allow_list(int *uids, int *size);
bool legacy_is_safe_mode();
bool legacy_uid_should_umount(int uid);
bool legacy_set_app_profile(const struct app_profile* profile);
bool legacy_get_app_profile(char* key, struct app_profile* profile);
bool legacy_set_su_enabled(bool enabled);
bool legacy_is_su_enabled();
bool legacy_is_KPM_enable();
bool legacy_get_hook_type(char* hook_type, size_t size);
void legacy_get_full_version(char* buff);
bool legacy_set_dynamic_manager(unsigned int size, const char* hash);
bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config);
bool legacy_clear_dynamic_manager();
bool legacy_get_managers_list(struct manager_list_info* info);
bool legacy_is_uid_scanner_enabled();
bool legacy_set_uid_scanner_enabled(bool enabled);
bool legacy_clear_uid_scanner_environment();
#endif //KERNELSU_KSU_H

View file

@ -0,0 +1,163 @@
//
// Created by shirkneko on 2025/11/3.
//
// Legacy Compatible
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <android/log.h>
#include <dirent.h>
#include <stdlib.h>
#include <limits.h>
#include "prelude.h"
#include "ksu.h"
#define KERNEL_SU_OPTION 0xDEADBEEF
#define CMD_GRANT_ROOT 0
#define CMD_BECOME_MANAGER 1
#define CMD_GET_VERSION 2
#define CMD_ALLOW_SU 3
#define CMD_DENY_SU 4
#define CMD_GET_SU_LIST 5
#define CMD_GET_DENY_LIST 6
#define CMD_CHECK_SAFEMODE 9
#define CMD_GET_APP_PROFILE 10
#define CMD_SET_APP_PROFILE 11
#define CMD_IS_UID_GRANTED_ROOT 12
#define CMD_IS_UID_SHOULD_UMOUNT 13
#define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15
#define CMD_GET_VERSION_FULL 0xC0FFEE1A
#define CMD_ENABLE_KPM 100
#define CMD_HOOK_TYPE 101
#define CMD_DYNAMIC_MANAGER 103
#define CMD_GET_MANAGERS 104
#define CMD_ENABLE_UID_SCANNER 105
static bool ksuctl(int cmd, void* arg1, void* arg2) {
int32_t result = 0;
int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
return result == KERNEL_SU_OPTION && rtn == -1;
}
struct ksu_version_info legacy_get_info()
{
int32_t version = -1;
int32_t flags = 0;
ksuctl(CMD_GET_VERSION, &version, &flags);
return (struct ksu_version_info){version, flags};
}
bool legacy_get_allow_list(int *uids, int *size) {
return ksuctl(CMD_GET_SU_LIST, uids, size);
}
bool legacy_is_safe_mode() {
return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL);
}
bool legacy_uid_should_umount(int uid) {
int should;
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should;
}
bool legacy_set_app_profile(const struct app_profile* profile) {
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL);
}
bool legacy_get_app_profile(char* key, struct app_profile* profile) {
return ksuctl(CMD_GET_APP_PROFILE, profile, NULL);
}
bool legacy_set_su_enabled(bool enabled) {
return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL);
}
bool legacy_is_su_enabled() {
int enabled = true;
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL);
return enabled;
}
bool legacy_is_KPM_enable() {
int enabled = false;
ksuctl(CMD_ENABLE_KPM, &enabled, NULL);
return enabled;
}
bool legacy_get_hook_type(char* hook_type, size_t size) {
if (hook_type == NULL || size == 0) {
return false;
}
static char cached_hook_type[16] = {0};
if (cached_hook_type[0] == '\0') {
if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) {
strcpy(cached_hook_type, "Unknown");
}
}
strncpy(hook_type, cached_hook_type, size - 1);
hook_type[size - 1] = '\0';
return true;
}
void legacy_get_full_version(char* buff) {
ksuctl(CMD_GET_VERSION_FULL, buff, NULL);
}
bool legacy_set_dynamic_manager(unsigned int size, const char* hash) {
if (hash == NULL) {
return false;
}
struct dynamic_manager_user_config config;
config.operation = DYNAMIC_MANAGER_OP_SET;
config.size = size;
strncpy(config.hash, hash, sizeof(config.hash) - 1);
config.hash[sizeof(config.hash) - 1] = '\0';
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
}
bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config) {
if (config == NULL) {
return false;
}
config->operation = DYNAMIC_MANAGER_OP_GET;
return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL);
}
bool legacy_clear_dynamic_manager() {
struct dynamic_manager_user_config config;
config.operation = DYNAMIC_MANAGER_OP_CLEAR;
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
}
bool legacy_get_managers_list(struct manager_list_info* info) {
if (info == NULL) {
return false;
}
return ksuctl(CMD_GET_MANAGERS, info, NULL);
}
bool legacy_is_uid_scanner_enabled() {
bool status = false;
ksuctl(CMD_ENABLE_UID_SCANNER, (void*)0, &status);
return status;
}
bool legacy_set_uid_scanner_enabled(bool enabled) {
return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)1, (void*)enabled);
}
bool legacy_clear_uid_scanner_environment() {
return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)2, NULL);
}

View file

@ -14,51 +14,51 @@
// Macros to simplify field setup
#define SET_BOOLEAN_FIELD(obj, cls, fieldName, value) do { \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \
GetEnvironment()->SetBooleanField(env, obj, field, value); \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \
GetEnvironment()->SetBooleanField(env, obj, field, value); \
} while(0)
#define SET_INT_FIELD(obj, cls, fieldName, value) do { \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \
GetEnvironment()->SetIntField(env, obj, field, value); \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \
GetEnvironment()->SetIntField(env, obj, field, value); \
} while(0)
#define SET_STRING_FIELD(obj, cls, fieldName, value) do { \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \
GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \
GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \
} while(0)
#define SET_OBJECT_FIELD(obj, cls, fieldName, value) do { \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \
GetEnvironment()->SetObjectField(env, obj, field, value); \
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \
GetEnvironment()->SetObjectField(env, obj, field, value); \
} while(0)
// Macros for creating Java objects
#define CREATE_JAVA_OBJECT(className) ({ \
jclass cls = GetEnvironment()->FindClass(env, className); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V"); \
GetEnvironment()->NewObject(env, cls, constructor); \
jclass cls = GetEnvironment()->FindClass(env, className); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V"); \
GetEnvironment()->NewObject(env, cls, constructor); \
})
// Macros for creating ArrayList
#define CREATE_ARRAYLIST() ({ \
jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "<init>", "()V"); \
GetEnvironment()->NewObject(env, arrayListCls, constructor); \
jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "<init>", "()V"); \
GetEnvironment()->NewObject(env, arrayListCls, constructor); \
})
// Macros for adding elements to an ArrayList
#define ADD_TO_LIST(list, item) do { \
jclass cls = GetEnvironment()->GetObjectClass(env, list); \
jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \
GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \
jclass cls = GetEnvironment()->GetObjectClass(env, list); \
jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \
GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \
} while(0)
// Macros for creating Java objects with parameter constructors
#define CREATE_JAVA_OBJECT_WITH_PARAMS(className, signature, ...) ({ \
jclass cls = GetEnvironment()->FindClass(env, className); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", signature); \
GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \
jclass cls = GetEnvironment()->FindClass(env, className); \
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", signature); \
GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \
})
#ifdef NDEBUG

View file

@ -1,94 +1,40 @@
package com.sukisu.ultra
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.system.Os
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
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 okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
import java.util.*
import java.util.Locale
@SuppressLint("StaticFieldLeak")
lateinit var ksuApp: KernelSUApplication
class KernelSUApplication : Application() {
private var currentActivity: Activity? = null
class KernelSUApplication : Application(), ViewModelStoreOwner {
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
currentActivity = activity
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
if (currentActivity == activity) {
currentActivity = null
}
}
}
override fun attachBaseContext(base: Context) {
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var context = base
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(base.resources.configuration)
config.setLocale(locale)
context = base.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
@SuppressLint("ObsoleteSdkInt")
override fun getResources(): Resources {
val resources = super.getResources()
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return createConfigurationContext(config).resources
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
return resources
}
lateinit var okhttpClient: OkHttpClient
private val appViewModelStore by lazy { ViewModelStore() }
override fun onCreate() {
super.onCreate()
ksuApp = this
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
// For faster response when first entering superuser or webui activity
val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java]
CoroutineScope(Dispatchers.Main).launch {
superUserViewModel.fetchAppList()
}
Platform.setHiddenApiExemptions()
@ -107,45 +53,20 @@ class KernelSUApplication : Application() {
if (!webroot.exists()) {
webroot.mkdir()
}
// Provide working env for rust's temp_dir()
Os.setenv("TMPDIR", cacheDir.absolutePath, true)
okhttpClient =
OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024))
.addInterceptor { block ->
block.proceed(
block.request().newBuilder()
.header("User-Agent", "SukiSU/${BuildConfig.VERSION_CODE}")
.header("Accept-Language", Locale.getDefault().toLanguageTag()).build()
)
}.build()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
applyLanguageSetting()
}
@SuppressLint("ObsoleteSdkInt")
private fun applyLanguageSetting() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
// 添加刷新当前Activity的方法
fun refreshCurrentActivity() {
currentActivity?.let { activity ->
val intent = activity.intent
activity.finish()
val options = ActivityOptions.makeCustomAnimation(
activity, android.R.anim.fade_in, android.R.anim.fade_out
)
activity.startActivity(intent, options.toBundle())
}
}
}
override val viewModelStore: ViewModelStore
get() = appViewModelStore
}

View file

@ -16,23 +16,22 @@ object Natives {
// 10946: add capabilities
// 10977: change groups_count and groups to avoid overflow write
// 11071: Fix the issue of failing to set a custom SELinux type.
const val MINIMAL_SUPPORTED_KERNEL = 11071
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5"
// 11640: Support query working mode, LKM or GKI
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
// 12143: breaking: new supercall impl
const val MINIMAL_SUPPORTED_KERNEL = 12143
// 12040: Support disable sucompat mode
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.8"
const val MINIMAL_SUPPORTED_KPM = 12800
const val MINIMAL_SUPPORTED_DYNAMIC_MANAGER = 13215
const val MINIMAL_SUPPORTED_UID_SCANNER = 13347
const val MINIMAL_NEW_IOCTL_KERNEL = 13490
const val ROOT_UID = 0
const val ROOT_GID = 0
@ -63,11 +62,9 @@ object Natives {
init {
System.loadLibrary("zakosign")
System.loadLibrary("zako")
System.loadLibrary("kernelsu")
}
// become root manager, return true if success.
external fun becomeManager(pkg: String?): Boolean
val version: Int
external get
@ -81,6 +78,9 @@ object Natives {
val isLkmMode: Boolean
external get
val isManager: Boolean
external get
external fun uidShouldUmount(uid: Int): Boolean
/**
@ -99,6 +99,25 @@ object Natives {
*/
external fun isSuEnabled(): Boolean
external fun setSuEnabled(enabled: Boolean): Boolean
/**
* Kernel module umount can be disabled temporarily.
* 0: disabled
* 1: enabled
* negative : error
*/
external fun isKernelUmountEnabled(): Boolean
external fun setKernelUmountEnabled(enabled: Boolean): Boolean
/**
* Enhanced security can be enabled/disabled.
* 0: disabled
* 1: enabled
* negative : error
*/
external fun isEnhancedSecurityEnabled(): Boolean
external fun setEnhancedSecurityEnabled(enabled: Boolean): Boolean
external fun isKPMEnabled(): Boolean
external fun getHookType(): String
@ -106,7 +125,6 @@ object Natives {
* Get SUSFS feature status from kernel
* @return SusfsFeatureStatus object containing all feature states, or null if failed
*/
external fun getSusfsFeatureStatus(): SusfsFeatureStatus?
/**
* Set dynamic managerature configuration
@ -138,6 +156,28 @@ object Natives {
// 模块签名验证
external fun verifyModuleSignature(modulePath: String): Boolean
/**
* Check if UID scanner is currently enabled
* @return true if UID scanner is enabled, false otherwise
*/
external fun isUidScannerEnabled(): Boolean
/**
* Enable or disable UID scanner
* @param enabled true to enable, false to disable
* @return true if operation was successful, false otherwise
*/
external fun setUidScannerEnabled(enabled: Boolean): Boolean
/**
* Clear UID scanner environment (force exit)
* This will forcefully stop all UID scanner operations and clear the environment
* @return true if operation was successful, false otherwise
*/
external fun clearUidScannerEnvironment(): Boolean
external fun getUserName(uid: Int): String?
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
private const val NOBODY_UID = 9999
@ -159,31 +199,10 @@ object Natives {
}
fun requireNewKernel(): Boolean {
if (version < MINIMAL_SUPPORTED_KERNEL) return true
if (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) return true
return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL)
}
@Immutable
@Parcelize
@Keep
data class SusfsFeatureStatus(
val statusSusPath: Boolean = false,
val statusSusMount: Boolean = false,
val statusAutoDefaultMount: Boolean = false,
val statusAutoBindMount: Boolean = false,
val statusSusKstat: Boolean = false,
val statusTryUmount: Boolean = false,
val statusAutoTryUmountBind: Boolean = false,
val statusSpoofUname: Boolean = false,
val statusEnableLog: Boolean = false,
val statusHideSymbols: Boolean = false,
val statusSpoofCmdline: Boolean = false,
val statusOpenRedirect: Boolean = false,
val statusMagicMount: Boolean = false,
val statusOverlayfsAutoKstat: Boolean = false,
val statusSusSu: Boolean = false
) : Parcelable
@Immutable
@Parcelize
@Keep

View file

@ -1,137 +1,75 @@
package com.sukisu.ultra.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageInfo
import android.os.*
import android.util.Log
import com.topjohnwu.superuser.ipc.RootService
import rikka.parcelablelist.ParcelableListSlice
import java.lang.reflect.Method
import com.sukisu.zako.IKsuInterface
/**
* @author ShirkNeko
* @date 2025/7/2.
* @date 2025/10/17.
*/
class KsuService : RootService() {
companion object {
private const val TAG = "KsuService"
private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface"
private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0
private val TAG = "KsuService"
private val cacheLock = Object()
private var _all: List<PackageInfo>? = null
private val allPackages: List<PackageInfo>
get() = synchronized(cacheLock) {
_all ?: loadAllPackages().also { _all = it }
}
private fun loadAllPackages(): List<PackageInfo> {
val tmp = arrayListOf<PackageInfo>()
for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) {
val userId = user.getUserIdCompat()
tmp += getInstalledPackagesAsUser(userId)
}
return tmp
}
interface IKsuInterface : IInterface {
fun getPackages(flags: Int): ParcelableListSlice<PackageInfo>
}
internal inner class Stub : IKsuInterface.Stub() {
override fun getPackageCount(): Int = allPackages.size
abstract class Stub : Binder(), IKsuInterface {
init {
attachInterface(this, DESCRIPTOR)
}
companion object {
fun asInterface(obj: IBinder?): IKsuInterface? {
if (obj == null) return null
val iin = obj.queryLocalInterface(DESCRIPTOR)
return if (iin != null && iin is IKsuInterface) {
iin
} else {
Proxy(obj)
}
}
}
override fun asBinder(): IBinder = this
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
val descriptor = DESCRIPTOR
when (code) {
INTERFACE_TRANSACTION -> {
reply?.writeString(descriptor)
return true
}
TRANSACTION_GET_PACKAGES -> {
data.enforceInterface(descriptor)
val flagsArg = data.readInt()
val result = getPackages(flagsArg)
reply?.writeNoException()
reply?.writeInt(1)
result.writeToParcel(reply!!, Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
return true
}
}
return super.onTransact(code, data, reply, flags)
}
private class Proxy(private val mRemote: IBinder) : IKsuInterface {
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val data = Parcel.obtain()
val reply = Parcel.obtain()
return try {
data.writeInterfaceToken(DESCRIPTOR)
data.writeInt(flags)
mRemote.transact(TRANSACTION_GET_PACKAGES, data, reply, 0)
reply.readException()
if (reply.readInt() != 0) {
@Suppress("UNCHECKED_CAST")
ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice<PackageInfo>
} else {
ParcelableListSlice(emptyList<PackageInfo>())
}
} finally {
reply.recycle()
data.recycle()
}
}
override fun asBinder(): IBinder = mRemote
override fun getPackages(start: Int, maxCount: Int): List<PackageInfo> {
val list = allPackages
val end = (start + maxCount).coerceAtMost(list.size)
return if (start >= list.size) emptyList()
else list.subList(start, end)
}
}
inner class KsuInterfaceImpl : Stub() {
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val list = getInstalledPackagesAll(flags)
Log.i(TAG, "getPackages: ${list.size}")
return ParcelableListSlice(list)
}
}
override fun onBind(intent: Intent): IBinder = Stub()
override fun onBind(intent: Intent): IBinder {
return KsuInterfaceImpl()
}
private fun getUserIds(): List<Int> {
val result = mutableListOf<Int>()
val um = getSystemService(USER_SERVICE) as UserManager
val userProfiles = um.userProfiles
for (userProfile in userProfiles) {
result.add(userProfile.hashCode())
}
return result
}
private fun getInstalledPackagesAll(flags: Int): ArrayList<PackageInfo> {
val packages = ArrayList<PackageInfo>()
for (userId in getUserIds()) {
Log.i(TAG, "getInstalledPackagesAll: $userId")
packages.addAll(getInstalledPackagesAsUser(flags, userId))
}
return packages
}
private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {
@SuppressLint("PrivateApi")
private fun getInstalledPackagesAsUser(userId: Int): List<PackageInfo> {
return try {
val pm = packageManager
val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod(
val m = pm.javaClass.getDeclaredMethod(
"getInstalledPackagesAsUser",
Int::class.java,
Int::class.java
)
@Suppress("UNCHECKED_CAST")
getInstalledPackagesAsUser.invoke(pm, flags, userId) as List<PackageInfo>
m.invoke(pm, 0, userId) as List<PackageInfo>
} catch (e: Throwable) {
Log.e(TAG, "err", e)
ArrayList()
Log.e(TAG, "getInstalledPackagesAsUser", e)
emptyList()
}
}
private fun UserHandle.getUserIdCompat(): Int {
return try {
javaClass.getDeclaredField("identifier").apply { isAccessible = true }.getInt(this)
} catch (_: NoSuchFieldException) {
javaClass.getDeclaredMethod("getIdentifier").invoke(this) as Int
} catch (e: Throwable) {
Log.e("KsuService", "getUserIdCompat", e)
0
}
}
}

View file

@ -1,7 +1,8 @@
package com.sukisu.ultra.ui
import android.content.Context
import android.content.res.Configuration
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
@ -9,13 +10,10 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry
@ -26,6 +24,8 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.KernelSUTheme
@ -34,11 +34,11 @@ import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform
import io.sukisu.ultra.UltraToolInstall
import com.sukisu.ultra.ui.component.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import zako.zako.zako.zakoui.activity.component.BottomBar
import zako.zako.zako.zakoui.activity.util.*
import com.sukisu.ultra.ui.activity.component.BottomBar
import com.sukisu.ultra.ui.activity.util.*
class MainActivity : ComponentActivity() {
private lateinit var superUserViewModel: SuperUserViewModel
@ -50,21 +50,18 @@ class MainActivity : ComponentActivity() {
val showKpmInfo: Boolean = false
)
private lateinit var themeChangeObserver: ThemeChangeContentObserver
private var showConfirmationDialog = mutableStateOf(false)
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
// 添加标记避免重复初始化
private lateinit var themeChangeObserver: ThemeChangeContentObserver
private var isInitialized = false
override fun attachBaseContext(newBase: Context) {
val context = LocaleUtils.applyLocale(newBase)
super.attachBaseContext(context)
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(newBase?.let { LocaleHelper.applyLanguage(it) })
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
// 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(this)
// 应用自定义 DPI
DisplayUtils.applyCustomDpi(this)
@ -77,6 +74,11 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val isManager = Natives.isManager
if (isManager && !Natives.requireNewKernel()) {
install()
}
// 使用标记控制初始化流程
if (!isInitialized) {
initializeViewModels()
@ -84,6 +86,39 @@ class MainActivity : ComponentActivity() {
isInitialized = true
}
// Check if launched with a ZIP file
val zipUri: ArrayList<Uri>? = when (intent?.action) {
Intent.ACTION_SEND -> {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
uri?.let { arrayListOf(it) }
}
Intent.ACTION_SEND_MULTIPLE -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
}
else -> when {
intent?.data != null -> arrayListOf(intent.data!!)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
intent.getParcelableArrayListExtra("uris", Uri::class.java)
}
else -> {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra("uris")
}
}
}
setContent {
KernelSUTheme {
val navController = rememberNavController()
@ -94,6 +129,38 @@ class MainActivity : ComponentActivity() {
BottomBarDestination.entries.map { it.direction.route }.toSet()
}
val navigator = navController.rememberDestinationsNavigator()
InstallConfirmationDialog(
show = showConfirmationDialog.value,
zipFiles = pendingZipFiles.value,
onConfirm = { confirmedFiles ->
showConfirmationDialog.value = false
UltraActivityUtils.navigateToFlashScreen(this, confirmedFiles, navigator)
},
onDismiss = {
showConfirmationDialog.value = false
pendingZipFiles.value = emptyList()
finish()
}
)
LaunchedEffect(zipUri) {
if (!zipUri.isNullOrEmpty()) {
// 检测 ZIP 文件类型并显示确认对话框
lifecycleScope.launch {
UltraActivityUtils.detectZipTypeAndShowConfirmation(this@MainActivity, zipUri) { infos ->
if (infos.isNotEmpty()) {
pendingZipFiles.value = infos
showConfirmationDialog.value = true
} else {
finish()
}
}
}
}
}
val showBottomBar = when (currentDestination?.route) {
ExecuteModuleActionScreenDestination.route -> false
else -> true
@ -187,32 +254,17 @@ class MainActivity : ComponentActivity() {
}
}
lifecycleScope.launch {
try {
homeViewModel.initializeData()
} catch (e: Exception) {
e.printStackTrace()
}
}
// 数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
val isManager = Natives.becomeManager(packageName)
if (isManager) {
install()
UltraToolInstall.tryToInstall()
}
}
override fun onResume() {
try {
super.onResume()
LocaleUtils.applyLanguageSetting(this)
ThemeUtils.onActivityResume()
// 仅在需要时刷新数据
@ -228,7 +280,6 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
homeViewModel.initializeData()
DataRefreshUtils.refreshData(lifecycleScope)
} catch (e: Exception) {
e.printStackTrace()
@ -253,13 +304,4 @@ class MainActivity : ComponentActivity() {
e.printStackTrace()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
try {
super.onConfigurationChanged(newConfig)
LocaleUtils.applyLanguageSetting(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View file

@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.activity.component
package com.sukisu.ultra.ui.activity.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
@ -15,21 +15,20 @@ import com.ramcosta.composedestinations.spec.RouteOrDirection
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.MainActivity
import com.sukisu.ultra.ui.activity.util.*
import com.sukisu.ultra.ui.activity.util.AppData.getKpmVersionUse
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import zako.zako.zako.zakoui.activity.util.AppData
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse
import com.sukisu.ultra.ui.util.*
@SuppressLint("ContextCastToActivity")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName)
val isFullFeatured = AppData.isFullFeatured()
val kpmVersion = getKpmVersionUse()
val cardColor = MaterialTheme.colorScheme.surfaceContainer
val activity = LocalContext.current as MainActivity
@ -40,9 +39,9 @@ fun BottomBar(navController: NavHostController) {
val showKpmInfo = settings.showKpmInfo
// 收集计数数据
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState()
val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState()
val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState()
NavigationBar(

View file

@ -1,8 +1,9 @@
package zako.zako.zako.zakoui.activity.util
package com.sukisu.ultra.ui.activity.util
import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
import androidx.core.content.edit
import com.sukisu.ultra.ui.MainActivity
import com.sukisu.ultra.ui.theme.CardConfig
@ -56,7 +57,7 @@ object ThemeUtils {
}
activity.contentResolver.registerContentObserver(
android.provider.Settings.System.getUriFor("ui_night_mode"),
Settings.System.getUriFor("ui_night_mode"),
false,
contentObserver
)

View file

@ -0,0 +1,236 @@
package com.sukisu.ultra.ui.activity.util
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable
import androidx.lifecycle.LifecycleCoroutineScope
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.MainActivity
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.*
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.ui.component.ZipFileDetector
import com.sukisu.ultra.ui.component.ZipFileInfo
import com.sukisu.ultra.ui.component.ZipType
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.sukisu.ultra.ui.screen.FlashIt
import kotlinx.coroutines.withContext
import androidx.core.content.edit
object AnimatedBottomBar {
@Composable
fun AnimatedBottomBarWrapper(
showBottomBar: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = showBottomBar,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
) {
content()
}
}
}
object UltraActivityUtils {
suspend fun detectZipTypeAndShowConfirmation(
activity: MainActivity,
zipUris: ArrayList<Uri>,
onResult: (List<ZipFileInfo>) -> Unit
) {
val infos = ZipFileDetector.detectAndParseZipFiles(activity, zipUris)
withContext(Dispatchers.Main) { onResult(infos) }
}
fun navigateToFlashScreen(
activity: MainActivity,
zipFiles: List<ZipFileInfo>,
navigator: DestinationsNavigator
) {
activity.lifecycleScope.launch {
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
when {
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
if (kernelUris.size == 1 && rootAvailable()) {
navigator.navigate(
InstallScreenDestination(
preselectedKernelUri = kernelUris.first().toString()
)
)
}
setAutoExitAfterFlash(activity)
}
moduleUris.isNotEmpty() -> {
navigator.navigate(
FlashScreenDestination(
FlashIt.FlashModules(ArrayList(moduleUris))
)
)
setAutoExitAfterFlash(activity)
}
}
}
}
private fun setAutoExitAfterFlash(activity: Context) {
activity.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
.edit {
putBoolean("auto_exit_after_flash", true)
}
}
}
object AppData {
object DataRefreshManager {
// 私有状态流
private val _superuserCount = MutableStateFlow(0)
private val _moduleCount = MutableStateFlow(0)
private val _kpmModuleCount = MutableStateFlow(0)
// 公开的只读状态流
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
/**
* 刷新所有数据计数
*/
fun refreshData() {
_superuserCount.value = getSuperuserCountUse()
_moduleCount.value = getModuleCountUse()
_kpmModuleCount.value = getKpmModuleCountUse()
}
}
/**
* 获取超级用户应用计数
*/
fun getSuperuserCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getSuperuserCount()
} catch (_: Exception) {
0
}
}
/**
* 获取模块计数
*/
fun getModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM模块计数
*/
fun getKpmModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
val kpmVersion = getKpmVersionUse()
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0
getKpmModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM版本
*/
fun getKpmVersionUse(): String {
return try {
if (!rootAvailable()) return ""
val version = getKpmVersion()
version.ifEmpty { "" }
} catch (e: Exception) {
"Error: ${e.message}"
}
}
/**
* 检查是否是完整功能模式
*/
fun isFullFeatured(): Boolean {
val isManager = Natives.isManager
return isManager && !Natives.requireNewKernel() && rootAvailable()
}
}
object DataRefreshUtils {
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
scope.launch(Dispatchers.IO) {
while (isActive) {
AppData.DataRefreshManager.refreshData()
delay(5000)
}
}
}
fun startSettingsMonitorCoroutine(
scope: LifecycleCoroutineScope,
activity: MainActivity,
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
) {
scope.launch(Dispatchers.IO) {
while (isActive) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
delay(1000)
}
}
}
fun refreshData(scope: LifecycleCoroutineScope) {
scope.launch {
AppData.DataRefreshManager.refreshData()
}
}
}
object DisplayUtils {
fun applyCustomDpi(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try {
val resources = context.resources
val metrics = resources.displayMetrics
metrics.density = customDpi / 160f
@Suppress("DEPRECATION")
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View file

@ -8,11 +8,16 @@ import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
@ -428,27 +433,36 @@ private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, di
@Composable
private fun MarkdownContent(content: String) {
val contentColor = LocalContentColor.current
val scrollState = rememberScrollState()
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE
}
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
}
},
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
update = {
Markwon.create(it.context).setMarkdown(it, content)
it.setTextColor(contentColor.toArgb())
}
)
}
.verticalScroll(
state = scrollState,
flingBehavior = ScrollableDefaults.flingBehavior()
)
.padding(12.dp)
) {
AndroidView(
factory = { context ->
TextView(context).apply {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE
}
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
},
update = {
Markwon.create(it.context).setMarkdown(it, content)
it.setTextColor(contentColor.toArgb())
}
)
}
}

View file

@ -1,223 +0,0 @@
package com.sukisu.ultra.ui.component
import android.net.Uri
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
import kotlinx.coroutines.launch
import kotlin.math.max
@Composable
fun ImageEditorDialog(
imageUri: Uri,
onDismiss: () -> Unit,
onConfirm: (Uri) -> Unit
) {
var scale by remember { mutableFloatStateOf(1f) }
var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
var lastScale by remember { mutableFloatStateOf(1f) }
var lastOffsetX by remember { mutableFloatStateOf(0f) }
var lastOffsetY by remember { mutableFloatStateOf(0f) }
var imageSize by remember { mutableStateOf(Size.Zero) }
var screenSize by remember { mutableStateOf(Size.Zero) }
val animatedScale by animateFloatAsState(
targetValue = scale,
label = "ScaleAnimation"
)
val animatedOffsetX by animateFloatAsState(
targetValue = offsetX,
label = "OffsetXAnimation"
)
val animatedOffsetY by animateFloatAsState(
targetValue = offsetY,
label = "OffsetYAnimation"
)
val updateTransformation = remember {
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
val scaleDiff = kotlin.math.abs(newScale - lastScale)
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
scale = newScale
offsetX = newOffsetX
offsetY = newOffsetY
lastScale = newScale
lastOffsetX = newOffsetX
lastOffsetY = newOffsetY
}
}
}
val scaleToFullScreen = remember {
{
if (imageSize.height > 0 && screenSize.height > 0) {
val newScale = screenSize.height / imageSize.height
updateTransformation(newScale, 0f, 0f)
}
}
}
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.9f))
.onSizeChanged { size ->
screenSize = Size(size.width.toFloat(), size.height.toFloat())
}
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUri)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.settings_custom_background),
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
scaleX = animatedScale,
scaleY = animatedScale,
translationX = animatedOffsetX,
translationY = animatedOffsetY
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scope.launch {
try {
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
val newOffsetX = if (maxOffsetX > 0) {
(offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
} else {
0f
}
val newOffsetY = if (maxOffsetY > 0) {
(offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
} else {
0f
}
updateTransformation(newScale, newOffsetX, newOffsetY)
} catch (_: Exception) {
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
}
}
}
}
.onSizeChanged { size ->
imageSize = Size(size.width.toFloat(), size.height.toFloat())
}
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(Alignment.TopCenter),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(
onClick = onDismiss,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.6f))
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
tint = Color.White
)
}
IconButton(
onClick = { scaleToFullScreen() },
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.6f))
) {
Icon(
imageVector = Icons.Default.Fullscreen,
contentDescription = stringResource(R.string.reprovision),
tint = Color.White
)
}
IconButton(
onClick = {
scope.launch {
try {
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
val savedUri = context.saveTransformedBackground(imageUri, transformation)
savedUri?.let { onConfirm(it) }
} catch (_: Exception) {
""
}
}
},
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.6f))
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(R.string.confirm),
tint = Color.White
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black.copy(alpha = 0.6f))
.padding(16.dp)
.align(Alignment.BottomCenter)
) {
Text(
text = stringResource(id = R.string.image_editor_hint),
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}

View file

@ -0,0 +1,441 @@
package com.sukisu.ultra.ui.component
import android.content.Context
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.GetApp
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.zip.ZipInputStream
enum class ZipType {
MODULE,
KERNEL,
UNKNOWN
}
data class ZipFileInfo(
val uri: Uri,
val type: ZipType,
val name: String = "",
val version: String = "",
val versionCode: String = "",
val author: String = "",
val description: String = "",
val kernelVersion: String = "",
val supported: String = ""
)
object ZipFileDetector {
fun detectZipType(context: Context, uri: Uri): ZipType {
return try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var hasModuleProp = false
var hasToolsFolder = false
var hasAnykernelSh = false
var entry = zipStream.nextEntry
while (entry != null) {
val entryName = entry.name.lowercase()
when {
entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
hasModuleProp = true
}
entryName.startsWith("tools/") || entryName == "tools" -> {
hasToolsFolder = true
}
entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
hasAnykernelSh = true
}
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
when {
hasModuleProp -> ZipType.MODULE
hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
else -> ZipType.UNKNOWN
}
}
} ?: ZipType.UNKNOWN
} catch (e: IOException) {
e.printStackTrace()
ZipType.UNKNOWN
}
}
fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo {
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE)
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var entry = zipStream.nextEntry
while (entry != null) {
if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) {
val reader = BufferedReader(InputStreamReader(zipStream))
val props = mutableMapOf<String, String>()
var line = reader.readLine()
while (line != null) {
if (line.contains("=") && !line.startsWith("#")) {
val parts = line.split("=", limit = 2)
if (parts.size == 2) {
props[parts[0].trim()] = parts[1].trim()
}
}
line = reader.readLine()
}
zipInfo = zipInfo.copy(
name = props["name"] ?: context.getString(R.string.unknown_module),
version = props["version"] ?: "",
versionCode = props["versionCode"] ?: "",
author = props["author"] ?: "",
description = props["description"] ?: ""
)
break
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return zipInfo
}
fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo {
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL)
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var entry = zipStream.nextEntry
while (entry != null) {
if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) {
val reader = BufferedReader(InputStreamReader(zipStream))
val props = mutableMapOf<String, String>()
var inPropertiesBlock = false
var line = reader.readLine()
while (line != null) {
if (line.contains("properties()")) {
inPropertiesBlock = true
} else if (inPropertiesBlock && line.contains("'; }")) {
inPropertiesBlock = false
} else if (inPropertiesBlock) {
val propertyLine = line.trim()
if (propertyLine.contains("=") && !propertyLine.startsWith("#")) {
val parts = propertyLine.split("=", limit = 2)
if (parts.size == 2) {
val key = parts[0].trim()
val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"")
when (key) {
"kernel.string" -> props["name"] = value
"supported.versions" -> props["supported"] = value
}
}
}
}
// 解析普通变量定义
if (line.contains("kernel.string=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"")
props["name"] = value
}
if (line.contains("supported.versions=") && !inPropertiesBlock) {
val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"")
props["supported"] = value
}
if (line.contains("kernel.version=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"")
props["version"] = value
}
if (line.contains("kernel.author=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"")
props["author"] = value
}
line = reader.readLine()
}
zipInfo = zipInfo.copy(
name = props["name"] ?: context.getString(R.string.unknown_kernel),
version = props["version"] ?: "",
author = props["author"] ?: "",
supported = props["supported"] ?: "",
kernelVersion = props["version"] ?: ""
)
break
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return zipInfo
}
suspend fun detectAndParseZipFiles(context: Context, zipUris: List<Uri>): List<ZipFileInfo> {
return withContext(Dispatchers.IO) {
val zipFileInfos = mutableListOf<ZipFileInfo>()
for (uri in zipUris) {
val zipType = detectZipType(context, uri)
val zipInfo = when (zipType) {
ZipType.MODULE -> parseModuleInfo(context, uri)
ZipType.KERNEL -> parseKernelInfo(context, uri)
ZipType.UNKNOWN -> ZipFileInfo(
uri = uri,
type = ZipType.UNKNOWN,
name = context.getString(R.string.unknown_file)
)
}
zipFileInfos.add(zipInfo)
}
zipFileInfos.filter { it.type != ZipType.UNKNOWN }
}
}
}
@Composable
fun InstallConfirmationDialog(
show: Boolean,
zipFiles: List<ZipFileInfo>,
onConfirm: (List<ZipFileInfo>) -> Unit,
onDismiss: () -> Unit
) {
if (show && zipFiles.isNotEmpty()) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = if (zipFiles.any { it.type == ZipType.KERNEL })
Icons.Default.Memory else Icons.Default.Extension,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = if (zipFiles.size == 1) {
context.getString(R.string.confirm_installation)
} else {
context.getString(R.string.confirm_multiple_installation, zipFiles.size)
},
style = MaterialTheme.typography.headlineSmall
)
}
},
text = {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(zipFiles.size) { index ->
val zipFile = zipFiles[index]
InstallItemCard(zipFile = zipFile)
}
}
},
confirmButton = {
Button(
onClick = { onConfirm(zipFiles) },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.GetApp,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(context.getString(R.string.install_confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(
context.getString(android.R.string.cancel),
color = MaterialTheme.colorScheme.onSurface
)
}
},
modifier = Modifier.widthIn(min = 320.dp, max = 560.dp)
)
}
}
@Composable
fun InstallItemCard(zipFile: ZipFileInfo) {
val context = LocalContext.current
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.elevatedCardColors(
containerColor = when (zipFile.type) {
ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f)
else -> MaterialTheme.colorScheme.surfaceVariant
}
),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = when (zipFile.type) {
ZipType.MODULE -> Icons.Default.Extension
ZipType.KERNEL -> Icons.Default.Memory
else -> Icons.AutoMirrored.Filled.Help
},
contentDescription = null,
tint = when (zipFile.type) {
ZipType.MODULE -> MaterialTheme.colorScheme.primary
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = zipFile.name.ifEmpty {
when (zipFile.type) {
ZipType.MODULE -> context.getString(R.string.unknown_module)
ZipType.KERNEL -> context.getString(R.string.unknown_kernel)
else -> context.getString(R.string.unknown_file)
}
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = when (zipFile.type) {
ZipType.MODULE -> context.getString(R.string.module_package)
ZipType.KERNEL -> context.getString(R.string.kernel_package)
else -> context.getString(R.string.unknown_package)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// 详细信息
if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() ||
zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
thickness = 0.5.dp
)
Spacer(modifier = Modifier.height(8.dp))
// 版本信息
if (zipFile.version.isNotEmpty()) {
InfoRow(
label = context.getString(R.string.version),
value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else ""
)
}
// 作者信息
if (zipFile.author.isNotEmpty()) {
InfoRow(
label = context.getString(R.string.author),
value = zipFile.author
)
}
// 描述信息 (仅模块)
if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) {
InfoRow(
label = context.getString(R.string.description),
value = zipFile.description
)
}
// 支持设备 (仅内核)
if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) {
InfoRow(
label = context.getString(R.string.supported_devices),
value = zipFile.supported
)
}
}
}
}
}
@Composable
fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = "$label:",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.widthIn(min = 60.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = value,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
}
}

View file

@ -8,7 +8,7 @@ import com.sukisu.ultra.ksuApp
fun KsuIsValid(
content: @Composable () -> Unit
) {
val isManager = Natives.becomeManager(ksuApp.packageName)
val isManager = Natives.isManager
val ksuVersion = if (isManager) Natives.version else null
if (ksuVersion != null) {

View file

@ -0,0 +1,250 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SuperDropdown(
items: List<String>,
selectedIndex: Int,
title: String,
summary: String? = null,
icon: ImageVector? = null,
enabled: Boolean = true,
showValue: Boolean = true,
maxHeight: Dp? = 400.dp,
colors: SuperDropdownColors = SuperDropdownDefaults.colors(),
leftAction: (@Composable () -> Unit)? = null,
onSelectedIndexChange: (Int) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
val selectedItemText = items.getOrNull(selectedIndex) ?: ""
val itemsNotEmpty = items.isNotEmpty()
val actualEnabled = enabled && itemsNotEmpty
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = actualEnabled) { showDialog = true }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.Top
) {
if (leftAction != null) {
leftAction()
} else if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor
)
if (summary != null) {
Spacer(modifier = Modifier.height(3.dp))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor
)
}
if (showValue && itemsNotEmpty) {
Spacer(modifier = Modifier.height(3.dp))
Text(
text = selectedItemText,
style = MaterialTheme.typography.bodyMedium,
color = if (actualEnabled) colors.valueColor else colors.disabledValueColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor,
modifier = Modifier.size(24.dp)
)
}
if (showDialog && itemsNotEmpty) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall
)
},
text = {
val dialogMaxHeight = maxHeight ?: 400.dp
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = dialogMaxHeight),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(items.size) { index ->
DropdownItem(
text = items[index],
isSelected = selectedIndex == index,
colors = colors,
onClick = {
onSelectedIndexChange(index)
showDialog = false
}
)
}
}
},
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(text = stringResource(id = android.R.string.cancel))
}
},
containerColor = colors.dialogBackgroundColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
}
}
@Composable
private fun DropdownItem(
text: String,
isSelected: Boolean,
colors: SuperDropdownColors,
onClick: () -> Unit
) {
val backgroundColor = if (isSelected) {
colors.selectedBackgroundColor
} else {
Color.Transparent
}
val contentColor = if (isSelected) {
colors.selectedContentColor
} else {
colors.contentColor
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor)
.clickable(onClick = onClick)
.padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = isSelected,
onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = colors.selectedContentColor,
unselectedColor = colors.contentColor
)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = contentColor,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = colors.selectedContentColor,
modifier = Modifier.size(20.dp)
)
}
}
}
@Immutable
data class SuperDropdownColors(
val titleColor: Color,
val summaryColor: Color,
val valueColor: Color,
val iconColor: Color,
val arrowColor: Color,
val disabledTitleColor: Color,
val disabledSummaryColor: Color,
val disabledValueColor: Color,
val disabledIconColor: Color,
val disabledArrowColor: Color,
val dialogBackgroundColor: Color,
val contentColor: Color,
val selectedContentColor: Color,
val selectedBackgroundColor: Color
)
object SuperDropdownDefaults {
@Composable
fun colors(
titleColor: Color = MaterialTheme.colorScheme.onSurface,
summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
iconColor: Color = MaterialTheme.colorScheme.primary,
arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
selectedContentColor: Color = MaterialTheme.colorScheme.primary,
selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
): SuperDropdownColors {
return SuperDropdownColors(
titleColor = titleColor,
summaryColor = summaryColor,
valueColor = valueColor,
iconColor = iconColor,
arrowColor = arrowColor,
disabledTitleColor = disabledTitleColor,
disabledSummaryColor = disabledSummaryColor,
disabledValueColor = disabledValueColor,
disabledIconColor = disabledIconColor,
disabledArrowColor = disabledArrowColor,
dialogBackgroundColor = dialogBackgroundColor,
contentColor = contentColor,
selectedContentColor = selectedContentColor,
selectedBackgroundColor = selectedBackgroundColor
)
}
}

View file

@ -21,7 +21,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
// 菜单项数据类
data class FabMenuItem(
val icon: ImageVector,
val labelRes: Int,
@ -29,7 +28,6 @@ data class FabMenuItem(
val onClick: () -> Unit
)
// 动画配置
object FabAnimationConfig {
const val ANIMATION_DURATION = 300
const val STAGGER_DELAY = 50
@ -53,23 +51,15 @@ fun VerticalExpandableFab(
) {
var isExpanded by remember { mutableStateOf(false) }
// 主按钮旋转动画
val rotationAngle by animateFloatAsState(
targetValue = if (isExpanded) 45f else 0f,
animationSpec = tween(
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
label = "mainButtonRotation"
)
// 主按钮缩放动画
val mainButtonScale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
animationSpec = tween(
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
label = "mainButtonScale"
)
@ -77,14 +67,9 @@ fun VerticalExpandableFab(
modifier = modifier.wrapContentSize(),
contentAlignment = Alignment.BottomEnd
) {
// 子菜单按钮
menuItems.forEachIndexed { index, menuItem ->
val animatedOffsetY by animateFloatAsState(
targetValue = if (isExpanded) {
-(buttonSpacing.value * (index + 1))
} else {
0f
},
targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f,
animationSpec = tween(
durationMillis = animationDurationMs,
delayMillis = if (isExpanded) {
@ -125,7 +110,6 @@ fun VerticalExpandableFab(
label = "fabAlpha$index"
)
// 子按钮容器(包含标签)
Row(
modifier = Modifier
.offset(y = animatedOffsetY.dp)
@ -134,7 +118,6 @@ fun VerticalExpandableFab(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
// 标签
AnimatedVisibility(
visible = isExpanded && animatedScale > 0.5f,
enter = slideInHorizontally(
@ -161,7 +144,6 @@ fun VerticalExpandableFab(
}
}
// 子按钮
SmallFloatingActionButton(
onClick = {
menuItem.onClick()
@ -193,15 +175,12 @@ fun VerticalExpandableFab(
}
}
// 主按钮
FloatingActionButton(
onClick = {
onMainButtonClick?.invoke()
isExpanded = !isExpanded
},
modifier = Modifier
.size(buttonSize)
.scale(mainButtonScale),
modifier = Modifier.size(buttonSize).scale(mainButtonScale),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 6.dp,
pressedElevation = 8.dp,
@ -221,7 +200,6 @@ fun VerticalExpandableFab(
}
}
// 预设菜单项
object FabMenuPresets {
fun getScrollMenuItems(
onScrollToTop: () -> Unit,

View file

@ -43,7 +43,7 @@ fun TemplateConfig(
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
readOnly = true,
label = { Text(stringResource(R.string.profile_template)) },

View file

@ -248,7 +248,12 @@ private fun AppProfileInner(
ProfileBox(mode, true) {
// template mode shouldn't change profile here!
if (it == Mode.Default || it == Mode.Custom) {
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
onProfileChange(
profile.copy(
rootUseDefault = it == Mode.Default,
rootTemplate = null
)
)
}
mode = it
}
@ -479,7 +484,10 @@ private fun ProfileBox(
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
private fun AppMenuBox(
packageName: String,
content: @Composable () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
val density = LocalDensity.current
@ -499,15 +507,15 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
content()
val (offsetX, offsetY) = with(density) {
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
(touchPoint.x.toDp()) to (-touchPoint.y.toDp())
}
DropdownMenu(
expanded = expanded,
offset = DpOffset(offsetX, -offsetY),
offset = DpOffset(offsetX, offsetY),
onDismissRequest = {
expanded = false
},
}
) {
AppMenuOption(
text = stringResource(id = R.string.launch_app),

View file

@ -1,5 +1,7 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.os.Parcelable
@ -30,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.activity.ComponentActivity
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
@ -48,6 +51,9 @@ import kotlinx.parcelize.Parcelize
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.util.module.ModuleOperationUtils
import com.sukisu.ultra.ui.util.module.ModuleUtils
/**
* @author ShirkNeko
@ -119,6 +125,29 @@ fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) {
@Destination<RootGraph>
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
val context = LocalContext.current
val shouldAutoExit = remember {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.getBoolean("auto_exit_after_flash", false)
}
// 是否通过从外部启动的模块安装
val isExternalInstall = remember {
when (flashIt) {
is FlashIt.FlashModule -> {
(context as? ComponentActivity)?.intent?.let { intent ->
intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND
} ?: false
}
is FlashIt.FlashModules -> {
(context as? ComponentActivity)?.intent?.let { intent ->
intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND
} ?: false
}
else -> false
}
}
var text by rememberSaveable { mutableStateOf("") }
var tempText: String
val logContent = rememberSaveable { StringBuilder() }
@ -203,8 +232,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
if (showReboot) {
text += "\n\n\n"
showFloatAction = true
// 如果是内部安装,显示重启按钮后不自动返回
if (isExternalInstall) {
return@flashModuleUpdate
}
}
hasUpdateCompleted = true
// 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回
if (isExternalInstall || shouldAutoExit) {
scope.launch {
kotlinx.coroutines.delay(1000)
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
}
(context as? ComponentActivity)?.finish()
}
}
}, onStdout = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
@ -297,6 +343,26 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
kotlinx.coroutines.delay(500)
navigator.navigate(FlashScreenDestination(nextFlashIt))
}
} else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) {
// 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回
scope.launch {
kotlinx.coroutines.delay(1000)
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
}
(context as? ComponentActivity)?.finish()
}
} else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) {
// 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回
scope.launch {
kotlinx.coroutines.delay(1000)
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
}
(context as? ComponentActivity)?.finish()
}
}
}, onStdout = {
tempText = "$it\n"
@ -319,14 +385,18 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
}
if (canGoBack) {
if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.navigate(ModuleScreenDestination)
if (isExternalInstall) {
(context as? ComponentActivity)?.finish()
} else {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.popBackStack()
if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.navigate(ModuleScreenDestination)
} else {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.popBackStack()
}
}
}
}
@ -619,7 +689,7 @@ private fun TopBar(
)
}
suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String {
suspend fun getModuleNameFromUri(context: Context, uri: Uri): String {
return withContext(Dispatchers.IO) {
try {
if (uri == Uri.EMPTY) {
@ -637,7 +707,7 @@ suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): St
@Parcelize
sealed class FlashIt : Parcelable {
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt()
data class FlashModule(val uri: Uri) : FlashIt()
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新
@ -666,6 +736,7 @@ fun flashIt(
flashIt.boot,
flashIt.lkm,
flashIt.ota,
flashIt.partition,
onFinish,
onStdout,
onStderr

View file

@ -50,20 +50,21 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.getSuSFSVersion
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import com.sukisu.ultra.ui.util.reboot
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.random.Random
/**
* @author ShirkNeko
* @date 2025/5/31.
* @date 2025/9/29.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Destination<RootGraph>(start = true)
@ -73,16 +74,35 @@ fun HomeScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<HomeViewModel>()
val coroutineScope = rememberCoroutineScope()
val pullRefreshState = rememberPullRefreshState(
refreshing = viewModel.isRefreshing,
onRefresh = {
viewModel.onPullRefresh(context)
}
)
LaunchedEffect(key1 = navigator) {
viewModel.loadUserSettings(context)
coroutineScope.launch {
viewModel.refreshAllData(context)
viewModel.loadCoreData()
delay(100)
viewModel.loadExtendedData(context)
}
// 启动数据变化监听
coroutineScope.launch {
while (true) {
delay(5000) // 每5秒检查一次
viewModel.autoRefreshIfNeeded(context)
}
}
}
LaunchedEffect(Unit) {
viewModel.loadUserSettings(context)
viewModel.initializeData()
viewModel.checkForUpdates(context)
// 监听数据刷新状态流
LaunchedEffect(viewModel.dataRefreshTrigger) {
viewModel.dataRefreshTrigger.collect { _ ->
// 数据刷新时的额外处理可以在这里添加
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@ -92,22 +112,14 @@ fun HomeScreen(navigator: DestinationsNavigator) {
topBar = {
TopBar(
scrollBehavior = scrollBehavior,
navigator = navigator
navigator = navigator,
isDataLoaded = viewModel.isCoreDataLoaded
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding ->
val pullRefreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = {
coroutineScope.launch {
viewModel.refreshAllData(context)
}
}
)
Box(
modifier = Modifier
.padding(innerPadding)
@ -121,50 +133,78 @@ fun HomeScreen(navigator: DestinationsNavigator) {
.padding(top = 12.dp, start = 16.dp, end = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
StatusCard(
systemStatus = viewModel.systemStatus,
onClickInstall = {
navigator.navigate(InstallScreenDestination)
}
)
// 状态卡片
if (viewModel.isCoreDataLoaded) {
StatusCard(
systemStatus = viewModel.systemStatus,
onClickInstall = {
navigator.navigate(InstallScreenDestination(preselectedKernelUri = null))
}
)
if (viewModel.systemStatus.requireNewKernel) {
WarningCard(
stringResource(id = R.string.require_kernel_version).format(
Natives.getSimpleVersionFull(),
Natives.MINIMAL_SUPPORTED_KERNEL_FULL
// 警告信息
if (viewModel.systemStatus.requireNewKernel) {
WarningCard(
stringResource(id = R.string.require_kernel_version).format(
Natives.getSimpleVersionFull(),
Natives.MINIMAL_SUPPORTED_KERNEL_FULL
)
)
}
if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) {
WarningCard(
stringResource(id = R.string.grant_root_failed)
)
}
// 只有在没有其他警告信息时才显示不兼容内核警告
val shouldShowWarnings = viewModel.systemStatus.requireNewKernel ||
(viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable)
if (Natives.version <= Natives.MINIMAL_NEW_IOCTL_KERNEL && !shouldShowWarnings && viewModel.systemStatus.ksuVersion != null) {
IncompatibleKernelCard()
Spacer(Modifier.height(12.dp))
}
}
// 更新检查
if (viewModel.isExtendedDataLoaded) {
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true)
if (checkUpdate) {
UpdateCard()
}
// 信息卡片
InfoCard(
systemInfo = viewModel.systemInfo,
isSimpleMode = viewModel.isSimpleMode,
isHideSusfsStatus = viewModel.isHideSusfsStatus,
isHideZygiskImplement = viewModel.isHideZygiskImplement,
showKpmInfo = viewModel.showKpmInfo,
lkmMode = viewModel.systemStatus.lkmMode,
)
}
if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) {
WarningCard(
stringResource(id = R.string.grant_root_failed)
)
}
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true)
if (checkUpdate) {
UpdateCard()
}
InfoCard(
systemInfo = viewModel.systemInfo,
isSimpleMode = viewModel.isSimpleMode,
isHideSusfsStatus = viewModel.isHideSusfsStatus,
isHideZygiskImplement = viewModel.isHideZygiskImplement,
showKpmInfo = viewModel.showKpmInfo,
lkmMode = viewModel.systemStatus.lkmMode,
)
if (!viewModel.isSimpleMode) {
if (!viewModel.isHideLinkCard) {
// 链接卡片
if (!viewModel.isSimpleMode && !viewModel.isHideLinkCard) {
ContributionCard()
DonateCard()
LearnMoreCard()
}
}
if (!viewModel.isExtendedDataLoaded) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
Spacer(Modifier.height(16.dp))
}
}
@ -231,7 +271,8 @@ fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
navigator: DestinationsNavigator
navigator: DestinationsNavigator,
isDataLoaded: Boolean = false
) {
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
@ -253,44 +294,47 @@ private fun TopBar(
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
actions = {
// SuSFS 配置按钮
if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) {
IconButton(onClick = {
navigator.navigate(SuSFSConfigScreenDestination)
}) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.susfs_config_setting_title)
)
}
}
// 重启按钮
var showDropdown by remember { mutableStateOf(false) }
KsuIsValid {
IconButton(onClick = {
showDropdown = true
}) {
Icon(
imageVector = Icons.Filled.PowerSettingsNew,
contentDescription = stringResource(id = R.string.reboot)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
if (isDataLoaded) {
// SuSFS 配置按钮
val susfsVersion = getSuSFSVersion()
if (susfsVersion.isNotEmpty() && !susfsVersion.startsWith("[-]") && SuSFSManager.isBinaryAvailable(context)) {
IconButton(onClick = {
navigator.navigate(SuSFSConfigScreenDestination)
}) {
RebootDropdownItem(id = R.string.reboot)
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.susfs_config_setting_title)
)
}
}
val pm =
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
// 重启按钮
var showDropdown by remember { mutableStateOf(false) }
KsuIsValid {
IconButton(onClick = {
showDropdown = true
}) {
Icon(
imageVector = Icons.Filled.PowerSettingsNew,
contentDescription = stringResource(id = R.string.reboot)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
RebootDropdownItem(id = R.string.reboot)
val pm =
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
}
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
}
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
}
}
}
@ -731,7 +775,7 @@ private fun InfoCard(
systemInfo.seLinuxStatus,
icon = Icons.Default.Security,
)
if (!isHideZygiskImplement && !isSimpleMode && systemInfo.zygiskImplement != "None") {
InfoCardItem(
stringResource(R.string.home_zygisk_implement),
@ -741,7 +785,6 @@ private fun InfoCard(
}
if (!isSimpleMode) {
// 根据showKpmInfo决定是否显示KPM信息
if (lkmMode != true && !showKpmInfo) {
val displayVersion =
if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) {
@ -785,21 +828,15 @@ private fun InfoCard(
private fun SuSFSInfoText(systemInfo: HomeViewModel.SystemInfo): String = buildString {
append(systemInfo.suSFSVersion)
val isSUS_SU = systemInfo.suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
val isKprobesHook = Natives.getHookType() == "Kprobes"
when {
isSUS_SU && isKprobesHook -> {
append(" (${systemInfo.suSFSVariant})")
if (systemInfo.susSUMode.isNotEmpty()) {
append(" ${stringResource(R.string.sus_su_mode)} ${systemInfo.susSUMode}")
}
}
Natives.getHookType() == "Manual" -> {
append(" (${stringResource(R.string.manual_hook)})")
}
Natives.getHookType() == "Inline" -> {
append(" (${stringResource(R.string.inline_hook)})")
}
else -> {
append(" (${Natives.getHookType()})")
}
@ -829,7 +866,7 @@ private fun StatusCardPreview() {
StatusCard(
HomeViewModel.SystemStatus(
isManager = true,
ksuVersion = 20000,
ksuVersion = 40000,
lkmMode = true,
kernelVersion = KernelVersion(5, 10, 101),
isRootAvailable = true
@ -858,6 +895,23 @@ private fun StatusCardPreview() {
}
}
@Composable
private fun IncompatibleKernelCard() {
val currentKver = remember { Natives.version }
val threshold = Natives.MINIMAL_NEW_IOCTL_KERNEL
val msg = stringResource(
id = R.string.incompatible_kernel_msg,
currentKver,
threshold
)
WarningCard(
message = msg,
color = MaterialTheme.colorScheme.error
)
}
@Preview
@Composable
private fun WarningCardPreview() {

View file

@ -1,8 +1,10 @@
package com.sukisu.ultra.ui.screen
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -17,7 +19,9 @@ import androidx.compose.foundation.selection.selectable
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.Input
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.*
@ -33,6 +37,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
@ -47,7 +52,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.SuperDropdown
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig
@ -56,6 +61,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog
/**
* @author ShirkNeko
@ -71,19 +77,49 @@ enum class KpmPatchOption {
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun InstallScreen(navigator: DestinationsNavigator) {
fun InstallScreen(
navigator: DestinationsNavigator,
preselectedKernelUri: String? = null
) {
val context = LocalContext.current
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) }
var showKpmPatchDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val summary = stringResource(R.string.horizon_kernel_summary)
// 处理预选的内核文件
LaunchedEffect(preselectedKernelUri) {
preselectedKernelUri?.let { uriString ->
try {
val preselectedUri = uriString.toUri()
val horizonMethod = InstallMethod.HorizonKernel(
uri = preselectedUri,
summary = summary
)
installMethod = horizonMethod
tempKernelUri = preselectedUri
if (isAbDevice) {
showSlotSelectionDialog = true
} else {
showKpmPatchDialog = true
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
if (showRebootDialog) {
RebootDialog(
show = true,
@ -103,6 +139,10 @@ fun InstallScreen(navigator: DestinationsNavigator) {
)
}
var partitionSelectionIndex by remember { mutableIntStateOf(0) }
var partitionsState by remember { mutableStateOf<List<String>>(emptyList()) }
var hasCustomSelected by remember { mutableStateOf(false) }
val onInstall = {
installMethod?.let { method ->
when (method) {
@ -119,10 +159,13 @@ fun InstallScreen(navigator: DestinationsNavigator) {
}
}
else -> {
val isOta = method is InstallMethod.DirectInstallToInactiveSlot
val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex)
val flashIt = FlashIt.FlashBoot(
boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot
ota = isOta,
partition = partitionSelection
)
navigator.navigate(FlashScreenDestination(flashIt))
}
@ -143,6 +186,20 @@ fun InstallScreen(navigator: DestinationsNavigator) {
summary = summary
)
installMethod = horizonMethod
if (preselectedKernelUri != null) {
showKpmPatchDialog = true
}
}
)
KpmPatchSelectionDialog(
show = showKpmPatchDialog,
currentOption = kpmPatchOption,
onDismiss = { showKpmPatchDialog = false },
onOptionSelected = { option ->
kpmPatchOption = option
showKpmPatchDialog = false
}
)
@ -165,6 +222,32 @@ fun InstallScreen(navigator: DestinationsNavigator) {
}
}
val selectLkmLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
val isKo = isKoFile(context, uri)
if (isKo) {
lkmSelection = LkmSelection.LkmUri(uri)
} else {
lkmSelection = LkmSelection.KmiNone
Toast.makeText(
context,
context.getString(R.string.install_only_support_ko_file),
Toast.LENGTH_SHORT
).show()
}
}
}
}
val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
})
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
@ -194,6 +277,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
showSlotSelectionDialog = true
} else {
installMethod = method
showKpmPatchDialog = true
}
} else {
installMethod = method
@ -204,35 +288,104 @@ fun InstallScreen(navigator: DestinationsNavigator) {
selectedMethod = installMethod
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
// 选择LKM直接安装分区
AnimatedVisibility(
visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
(lkmSelection as? LkmSelection.LkmUri)?.let {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
.padding(bottom = 12.dp),
) {
Text(
text = stringResource(
id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)"
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot
val suffix = produceState(initialValue = "", isOta) {
value = getSlotSuffix(isOta)
}.value
val partitions = produceState(initialValue = emptyList()) {
value = getAvailablePartitions()
}.value
val defaultPartition = produceState(initialValue = "") {
value = getDefaultPartition()
}.value
partitionsState = partitions
val displayPartitions = partitions.map { name ->
if (defaultPartition == name) "$name (default)" else name
}
val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0
if (!hasCustomSelected) partitionSelectionIndex = defaultIndex
SuperDropdown(
items = displayPartitions,
selectedIndex = partitionSelectionIndex,
title = "${stringResource(R.string.install_select_partition)} (${suffix})",
onSelectedIndexChange = { index ->
hasCustomSelected = true
partitionSelectionIndex = index
},
leftAction = {
Icon(
Icons.Default.Edit,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(end = 16.dp),
contentDescription = null
)
}
)
}
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 使用本地的LKM文件
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
) {
ListItem(
headlineContent = {
Text(stringResource(id = R.string.install_upload_lkm_file))
},
supportingContent = {
(lkmSelection as? LkmSelection.LkmUri)?.let {
Text(
stringResource(
id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)"
)
)
}
},
leadingContent = {
Icon(
Icons.AutoMirrored.Filled.Input,
contentDescription = null
)
},
modifier = Modifier
.fillMaxWidth()
.clickable { onLkmUpload() }
)
}
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
if (method.slot != null) {
@ -242,12 +395,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = stringResource(
@ -269,12 +416,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = when (kpmPatchOption) {
@ -316,6 +457,47 @@ fun InstallScreen(navigator: DestinationsNavigator) {
}
}
@Composable
private fun KpmPatchSelectionDialog(
show: Boolean,
currentOption: KpmPatchOption,
onDismiss: () -> Unit,
onOptionSelected: (KpmPatchOption) -> Unit
) {
if (show) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.kpm_patch_options)) },
text = {
Column {
Text(
text = stringResource(R.string.kpm_patch_description),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
KpmPatchOptionGroup(
selectedOption = currentOption,
onOptionChanged = onOptionSelected
)
}
},
confirmButton = {
TextButton(
onClick = { onOptionSelected(currentOption) }
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
}
@Composable
private fun RebootDialog(
show: Boolean,
@ -378,15 +560,15 @@ private fun SelectInstallMethod(
selectedMethod: InstallMethod? = null
) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val defaultPartitionName = produceState(initialValue = "boot") {
value = getDefaultPartition()
}.value
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
val selectFileTip = stringResource(
id = R.string.select_file_tip,
if (isInitBoot()) {
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
} else {
"boot"
}
id = R.string.select_file_tip, defaultPartitionName
)
val radioOptions = mutableListOf<InstallMethod>(
@ -404,6 +586,10 @@ private fun SelectInstallMethod(
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
LaunchedEffect(selectedMethod) {
selectedOption = selectedMethod
}
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
@ -479,7 +665,6 @@ private fun SelectInstallMethod(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
@ -518,7 +703,7 @@ private fun SelectInstallMethod(
bottom = 16.dp
)
) {
radioOptions.take(3).forEach { option ->
radioOptions.filter { it !is InstallMethod.HorizonKernel }.forEach { option ->
val interactionSource = remember { MutableInteractionSource() }
Surface(
color = if (option.javaClass == selectedOption?.javaClass)
@ -586,7 +771,6 @@ private fun SelectInstallMethod(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
@ -884,6 +1068,31 @@ private fun TopBar(
)
}
private fun isKoFile(context: Context, uri: Uri): Boolean {
val seg = uri.lastPathSegment ?: ""
if (seg.endsWith(".ko", ignoreCase = true)) return true
return try {
context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null
)?.use { cursor ->
val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (idx != -1 && cursor.moveToFirst()) {
val name = cursor.getString(idx)
name?.endsWith(".ko", ignoreCase = true) == true
} else {
false
}
} ?: false
} catch (_: Throwable) {
false
}
}
@Preview
@Composable
fun SelectInstallPreview() {

View file

@ -0,0 +1,941 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.*
import java.time.format.DateTimeFormatter
import android.os.Process.myUid
import androidx.core.content.edit
private val SPACING_SMALL = 4.dp
private val SPACING_MEDIUM = 8.dp
private val SPACING_LARGE = 16.dp
private const val PAGE_SIZE = 10000
private const val MAX_TOTAL_LOGS = 100000
private const val LOGS_PATCH = "/data/adb/ksu/log/sulog.log"
data class LogEntry(
val timestamp: String,
val type: LogType,
val uid: String,
val comm: String,
val details: String,
val pid: String,
val rawLine: String
)
data class LogPageInfo(
val currentPage: Int = 0,
val totalPages: Int = 0,
val totalLogs: Int = 0,
val hasMore: Boolean = false
)
enum class LogType(val displayName: String, val color: Color) {
SU_GRANT("SU_GRANT", Color(0xFF4CAF50)),
SU_EXEC("SU_EXEC", Color(0xFF2196F3)),
PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)),
SYSCALL("SYSCALL", Color(0xFF00BCD4)),
MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)),
UNKNOWN("UNKNOWN", Color(0xFF757575))
}
enum class LogExclType(val displayName: String, val color: Color) {
CURRENT_APP("Current app", Color(0xFF9E9E9E)),
PRCTL_STAR("prctl_*", Color(0xFF00BCD4)),
PRCTL_UNKNOWN("prctl_unknown", Color(0xFF00BCD4)),
SETUID("setuid", Color(0xFF00BCD4))
}
private val utcFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private val localFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private fun saveExcludedSubTypes(context: Context, types: Set<LogExclType>) {
val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE)
val nameSet = types.map { it.name }.toSet()
prefs.edit { putStringSet("excluded_subtypes", nameSet) }
}
private fun loadExcludedSubTypes(context: Context): Set<LogExclType> {
val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE)
val nameSet = prefs.getStringSet("excluded_subtypes", emptySet()) ?: emptySet()
return nameSet.mapNotNull { name ->
LogExclType.entries.firstOrNull { it.name == name }
}.toSet()
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun LogViewerScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
var logEntries by remember { mutableStateOf<List<LogEntry>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var filterType by rememberSaveable { mutableStateOf<LogType?>(null) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var showSearchBar by rememberSaveable { mutableStateOf(false) }
var pageInfo by remember { mutableStateOf(LogPageInfo()) }
var lastLogFileHash by remember { mutableStateOf("") }
val currentUid = remember { myUid().toString() }
val initialExcluded = remember {
loadExcludedSubTypes(context)
}
var excludedSubTypes by rememberSaveable { mutableStateOf(initialExcluded) }
LaunchedEffect(excludedSubTypes) {
saveExcludedSubTypes(context, excludedSubTypes)
}
val filteredEntries = remember(
logEntries, filterType, searchQuery, excludedSubTypes
) {
logEntries.filter { entry ->
val matchesSearch = searchQuery.isEmpty() ||
entry.comm.contains(searchQuery, ignoreCase = true) ||
entry.details.contains(searchQuery, ignoreCase = true) ||
entry.uid.contains(searchQuery, ignoreCase = true)
// 排除本应用
if (LogExclType.CURRENT_APP in excludedSubTypes && entry.uid == currentUid) return@filter false
// 排除 SYSCALL 子类型
if (entry.type == LogType.SYSCALL) {
val detail = entry.details
if (LogExclType.PRCTL_STAR in excludedSubTypes && detail.startsWith("Syscall: prctl") && !detail.startsWith("Syscall: prctl_unknown")) return@filter false
if (LogExclType.PRCTL_UNKNOWN in excludedSubTypes && detail.startsWith("Syscall: prctl_unknown")) return@filter false
if (LogExclType.SETUID in excludedSubTypes && detail.startsWith("Syscall: setuid")) return@filter false
}
// 普通类型筛选
val matchesFilter = filterType == null || entry.type == filterType
matchesFilter && matchesSearch
}
}
val loadingDialog = rememberLoadingDialog()
val confirmDialog = rememberConfirmDialog()
val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh ->
scope.launch {
if (isLoading) return@launch
isLoading = true
try {
loadLogsWithPagination(
page,
forceRefresh,
lastLogFileHash
) { entries, newPageInfo, newHash ->
logEntries = if (page == 0 || forceRefresh) {
entries
} else {
logEntries + entries
}
pageInfo = newPageInfo
lastLogFileHash = newHash
}
} finally {
isLoading = false
}
}
}
val onManualRefresh: () -> Unit = {
loadPage(0, true)
}
val loadNextPage: () -> Unit = {
if (pageInfo.hasMore && !isLoading) {
loadPage(pageInfo.currentPage + 1, false)
}
}
LaunchedEffect(Unit) {
while (true) {
delay(5_000)
if (!isLoading) {
scope.launch {
val hasNewLogs = checkForNewLogs(lastLogFileHash)
if (hasNewLogs) {
loadPage(0, true)
}
}
}
}
}
LaunchedEffect(Unit) {
loadPage(0, true)
}
Scaffold(
topBar = {
LogViewerTopBar(
scrollBehavior = scrollBehavior,
onBackClick = { navigator.navigateUp() },
showSearchBar = showSearchBar,
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
onSearchToggle = { showSearchBar = !showSearchBar },
onRefresh = onManualRefresh,
onClearLogs = {
scope.launch {
val result = confirmDialog.awaitConfirm(
title = context.getString(R.string.log_viewer_clear_logs),
content = context.getString(R.string.log_viewer_clear_logs_confirm)
)
if (result == ConfirmResult.Confirmed) {
loadingDialog.withLoading {
clearLogs()
loadPage(0, true)
}
snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared))
}
}
}
)
},
snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
LogControlPanel(
filterType = filterType,
onFilterTypeSelected = { filterType = it },
logCount = filteredEntries.size,
totalCount = logEntries.size,
pageInfo = pageInfo,
excludedSubTypes = excludedSubTypes,
onExcludeToggle = { excl ->
excludedSubTypes = if (excl in excludedSubTypes)
excludedSubTypes - excl
else
excludedSubTypes + excl
}
)
// 日志列表
if (isLoading && logEntries.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (filteredEntries.isEmpty()) {
EmptyLogState(
hasLogs = logEntries.isNotEmpty(),
onRefresh = onManualRefresh
)
} else {
LogList(
entries = filteredEntries,
pageInfo = pageInfo,
isLoading = isLoading,
onLoadMore = loadNextPage,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
private fun LogControlPanel(
filterType: LogType?,
onFilterTypeSelected: (LogType?) -> Unit,
logCount: Int,
totalCount: Int,
pageInfo: LogPageInfo,
excludedSubTypes: Set<LogExclType>,
onExcludeToggle: (LogExclType) -> Unit
) {
var isExpanded by rememberSaveable { mutableStateOf(true) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = getCardElevation()
) {
Column {
// 标题栏(点击展开/收起)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { isExpanded = !isExpanded }
.padding(SPACING_LARGE),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.settings),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Icon(
imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier.padding(horizontal = SPACING_LARGE)
) {
// 类型过滤
Text(
text = stringResource(R.string.log_viewer_filter_type),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) {
item {
FilterChip(
onClick = { onFilterTypeSelected(null) },
label = { Text(stringResource(R.string.log_viewer_all_types)) },
selected = filterType == null
)
}
items(LogType.entries.toTypedArray()) { type ->
FilterChip(
onClick = { onFilterTypeSelected(if (filterType == type) null else type) },
label = { Text(type.displayName) },
selected = filterType == type,
leadingIcon = {
Box(
modifier = Modifier
.size(8.dp)
.background(type.color, RoundedCornerShape(4.dp))
)
}
)
}
}
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
// 排除子类型
Text(
text = stringResource(R.string.log_viewer_exclude_subtypes),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) {
items(LogExclType.entries.toTypedArray()) { excl ->
val label = if (excl == LogExclType.CURRENT_APP)
stringResource(R.string.log_viewer_exclude_current_app)
else excl.displayName
FilterChip(
onClick = { onExcludeToggle(excl) },
label = { Text(label) },
selected = excl in excludedSubTypes,
leadingIcon = {
Box(
modifier = Modifier
.size(8.dp)
.background(excl.color, RoundedCornerShape(4.dp))
)
}
)
}
}
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
// 统计信息
Column(verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)) {
Text(
text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (pageInfo.totalPages > 0) {
Text(
text = stringResource(
R.string.log_viewer_page_info,
pageInfo.currentPage + 1,
pageInfo.totalPages,
pageInfo.totalLogs
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) {
Text(
text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
}
}
}
}
@Composable
private fun LogList(
entries: List<LogEntry>,
pageInfo: LogPageInfo,
isLoading: Boolean,
onLoadMore: () -> Unit,
modifier: Modifier = Modifier
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier,
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)
) {
items(entries) { entry ->
LogEntryCard(entry = entry)
}
// 加载更多按钮或加载指示器
if (pageInfo.hasMore) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
contentAlignment = Alignment.Center
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp)
)
} else {
Button(
onClick = onLoadMore,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Filled.ExpandMore,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(stringResource(R.string.log_viewer_load_more))
}
}
}
}
} else if (entries.isNotEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.log_viewer_all_logs_loaded),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@Composable
private fun LogEntryCard(entry: LogEntry) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded },
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Column(
modifier = Modifier.padding(SPACING_MEDIUM)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
Box(
modifier = Modifier
.size(12.dp)
.background(entry.type.color, RoundedCornerShape(6.dp))
)
Text(
text = entry.type.displayName,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold
)
}
Text(
text = entry.timestamp,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(SPACING_SMALL))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "UID: ${entry.uid}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "PID: ${entry.pid}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = entry.comm,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
maxLines = if (expanded) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis
)
if (entry.details.isNotEmpty()) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = entry.details,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = if (expanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis
)
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Text(
text = stringResource(R.string.log_viewer_raw_log),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = entry.rawLine,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@Composable
private fun EmptyLogState(
hasLogs: Boolean,
onRefresh: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(SPACING_LARGE)
) {
Icon(
imageVector = if (hasLogs) Icons.Filled.FilterList else Icons.Filled.Description,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(
if (hasLogs) R.string.log_viewer_no_matching_logs
else R.string.log_viewer_no_logs
),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Button(onClick = onRefresh) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(stringResource(R.string.log_viewer_refresh))
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LogViewerTopBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
onBackClick: () -> Unit,
showSearchBar: Boolean,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
onSearchToggle: () -> Unit,
onRefresh: () -> Unit,
onClearLogs: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
Column {
TopAppBar(
title = {
Text(
text = stringResource(R.string.log_viewer_title),
style = MaterialTheme.typography.titleLarge
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.log_viewer_back)
)
}
},
actions = {
IconButton(onClick = onSearchToggle) {
Icon(
imageVector = if (showSearchBar) Icons.Filled.SearchOff else Icons.Filled.Search,
contentDescription = stringResource(R.string.log_viewer_search)
)
}
IconButton(onClick = onRefresh) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.log_viewer_refresh)
)
}
IconButton(onClick = onClearLogs) {
Icon(
imageVector = Icons.Filled.DeleteSweep,
contentDescription = stringResource(R.string.log_viewer_clear_logs)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
AnimatedVisibility(
visible = showSearchBar,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
OutlinedTextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
placeholder = { Text(stringResource(R.string.log_viewer_search_placeholder)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = null
)
},
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { onSearchQueryChange("") }) {
Icon(
imageVector = Icons.Filled.Clear,
contentDescription = stringResource(R.string.log_viewer_clear_search)
)
}
}
},
singleLine = true
)
}
}
}
private suspend fun checkForNewLogs(
lastHash: String
): Boolean {
return withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
val logPath = "/data/adb/ksu/log/sulog.log"
val result = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'")
val currentHash = result.trim()
currentHash != lastHash && currentHash != "0 0"
} catch (_: Exception) {
false
}
}
}
private suspend fun loadLogsWithPagination(
page: Int,
forceRefresh: Boolean,
lastHash: String,
onLoaded: (List<LogEntry>, LogPageInfo, String) -> Unit
) {
withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
// 获取文件信息
val statResult = runCmd(shell, "stat -c '%Y %s' $LOGS_PATCH 2>/dev/null || echo '0 0'")
val currentHash = statResult.trim()
if (!forceRefresh && currentHash == lastHash && currentHash != "0 0") {
withContext(Dispatchers.Main) {
onLoaded(emptyList(), LogPageInfo(), currentHash)
}
return@withContext
}
// 获取总行数
val totalLinesResult = runCmd(shell, "wc -l < $LOGS_PATCH 2>/dev/null || echo '0'")
val totalLines = totalLinesResult.trim().toIntOrNull() ?: 0
if (totalLines == 0) {
withContext(Dispatchers.Main) {
onLoaded(emptyList(), LogPageInfo(), currentHash)
}
return@withContext
}
// 限制最大日志数量
val effectiveTotal = minOf(totalLines, MAX_TOTAL_LOGS)
val totalPages = (effectiveTotal + PAGE_SIZE - 1) / PAGE_SIZE
// 计算要读取的行数范围
val startLine = if (page == 0) {
maxOf(1, totalLines - effectiveTotal + 1)
} else {
val skipLines = page * PAGE_SIZE
maxOf(1, totalLines - effectiveTotal + 1 + skipLines)
}
val endLine = minOf(startLine + PAGE_SIZE - 1, totalLines)
if (startLine > totalLines) {
withContext(Dispatchers.Main) {
onLoaded(emptyList(), LogPageInfo(page, totalPages, effectiveTotal, false), currentHash)
}
return@withContext
}
val result = runCmd(shell, "sed -n '${startLine},${endLine}p' $LOGS_PATCH 2>/dev/null || echo ''")
val entries = parseLogEntries(result)
val hasMore = endLine < totalLines
val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore)
withContext(Dispatchers.Main) {
onLoaded(entries, pageInfo, currentHash)
}
} catch (_: Exception) {
withContext(Dispatchers.Main) {
onLoaded(emptyList(), LogPageInfo(), lastHash)
}
}
}
}
private suspend fun clearLogs() {
withContext(Dispatchers.IO) {
try {
val shell = getRootShell()
runCmd(shell, "echo '' > $LOGS_PATCH")
} catch (_: Exception) {
}
}
}
private fun parseLogEntries(logContent: String): List<LogEntry> {
if (logContent.isBlank()) return emptyList()
val entries = logContent.lines()
.filter { it.isNotBlank() && it.startsWith("[") }
.mapNotNull { line ->
try {
parseLogLine(line)
} catch (_: Exception) {
null
}
}
return entries.reversed()
}
private fun utcToLocal(utc: String): String {
return try {
val instant = LocalDateTime.parse(utc, utcFormatter).atOffset(ZoneOffset.UTC).toInstant()
val local = instant.atZone(ZoneId.systemDefault())
local.format(localFormatter)
} catch (_: Exception) {
utc
}
}
private fun parseLogLine(line: String): LogEntry? {
// 解析格式: [timestamp] TYPE: UID=xxx COMM=xxx ...
val timestampRegex = """\[(.*?)]""".toRegex()
val timestampMatch = timestampRegex.find(line) ?: return null
val timestamp = utcToLocal(timestampMatch.groupValues[1])
val afterTimestamp = line.substring(timestampMatch.range.last + 1).trim()
val parts = afterTimestamp.split(":")
if (parts.size < 2) return null
val typeStr = parts[0].trim()
val type = when (typeStr) {
"SU_GRANT" -> LogType.SU_GRANT
"SU_EXEC" -> LogType.SU_EXEC
"PERM_CHECK" -> LogType.PERM_CHECK
"SYSCALL" -> LogType.SYSCALL
"MANAGER_OP" -> LogType.MANAGER_OP
else -> LogType.UNKNOWN
}
val details = parts[1].trim()
val uid: String = extractValue(details, "UID") ?: ""
val comm: String = extractValue(details, "COMM") ?: ""
val pid: String = extractValue(details, "PID") ?: ""
// 构建详细信息字符串
val detailsStr = when (type) {
LogType.SU_GRANT -> {
val method: String = extractValue(details, "METHOD") ?: ""
"Method: $method"
}
LogType.SU_EXEC -> {
val target: String = extractValue(details, "TARGET") ?: ""
val result: String = extractValue(details, "RESULT") ?: ""
"Target: $target, Result: $result"
}
LogType.PERM_CHECK -> {
val result: String = extractValue(details, "RESULT") ?: ""
"Result: $result"
}
LogType.SYSCALL -> {
val syscall = extractValue(details, "SYSCALL") ?: ""
val args = extractValue(details, "ARGS") ?: ""
"Syscall: $syscall, Args: $args"
}
LogType.MANAGER_OP -> {
val op: String = extractValue(details, "OP") ?: ""
val managerUid: String = extractValue(details, "MANAGER_UID") ?: ""
val targetUid: String = extractValue(details, "TARGET_UID") ?: ""
"Operation: $op, Manager UID: $managerUid, Target UID: $targetUid"
}
else -> details
}
return LogEntry(
timestamp = timestamp,
type = type,
uid = uid,
comm = comm,
details = detailsStr,
pid = pid,
rawLine = line
)
}
private fun extractValue(text: String, key: String): String? {
val regex = """$key=(\S+)""".toRegex()
return regex.find(text)?.groupValues?.get(1)
}

View file

@ -75,6 +75,10 @@ import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.module.ModuleModify
import com.sukisu.ultra.ui.util.module.ModuleOperationUtils
import com.sukisu.ultra.ui.util.module.ModuleUtils
import com.sukisu.ultra.ui.util.module.verifyModuleSignature
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import com.sukisu.ultra.ui.webui.WebUIActivity
import com.sukisu.ultra.ui.webui.WebUIXActivity
@ -84,7 +88,6 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
// 菜单项数据类
data class ModuleBottomSheetMenuItem(
val icon: ImageVector,
val titleRes: Int,
@ -93,7 +96,7 @@ data class ModuleBottomSheetMenuItem(
/**
* @author ShirkNeko
* @date 2025/5/31.
* @date 2025/9/29.
*/
@SuppressLint("ResourceType", "AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@ -102,24 +105,21 @@ data class ModuleBottomSheetMenuItem(
fun ModuleScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<ModuleViewModel>()
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings",MODE_PRIVATE)
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val confirmDialog = rememberConfirmDialog()
var lastClickTime by remember { mutableStateOf(0L) }
// 签名验证弹窗状态
var showSignatureDialog by remember { mutableStateOf(false) }
var signatureDialogMessage by remember { mutableStateOf("") }
var isForceVerificationFailed by remember { mutableStateOf(false) }
var pendingInstallAction by remember { mutableStateOf<(() -> Unit)?>(null) }
// 初始化缓存系统
LaunchedEffect(Unit) {
viewModel.initializeCache(context)
}
// BottomSheet状态
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
@ -280,7 +280,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
viewModel.sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false)
@ -291,7 +290,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val isSafeMode = Natives.isSafeMode
val hasMagisk = hasMagisk()
val hideInstallButton = isSafeMode || hasMagisk
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@ -300,7 +298,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
contract = ActivityResultContracts.StartActivityForResult()
) { viewModel.fetchModuleList() }
// BottomSheet菜单项
val bottomSheetMenuItems = remember {
listOf(
ModuleBottomSheetMenuItem(
@ -478,7 +475,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
}
}
// BottomSheet
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
@ -620,7 +616,6 @@ private fun ModuleBottomSheetContent(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 排序选项
Column(
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
@ -682,7 +677,6 @@ private fun ModuleBottomSheetContent(
@Composable
private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) {
// 添加交互状态
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
@ -810,7 +804,6 @@ private fun ModuleList(
return
}
// changelog is not empty, show it and wait for confirm
val confirmResult = confirmDialog.awaitConfirm(
changelogText,
content = changelog,
@ -901,6 +894,7 @@ private fun ModuleList(
reboot()
}
}
PullToRefreshBox(
modifier = boxModifier,
onRefresh = {
@ -1003,7 +997,6 @@ private fun ModuleList(
}
)
// fix last item shadow incomplete in LazyColumn
Spacer(Modifier.height(1.dp))
}
}
@ -1011,7 +1004,6 @@ private fun ModuleList(
}
DownloadListener(context, onInstallModule)
}
}
@ -1044,7 +1036,6 @@ fun ModuleItem(
val indication = LocalIndication.current
val viewModel = viewModel<ModuleViewModel>()
// 使用缓存系统获取模块大小
val sizeStr = remember(module.dirId) {
viewModel.getModuleSize(module.dirId)
}
@ -1152,10 +1143,8 @@ fun ModuleItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
},
onClick = { },
onLongClick = {
// 长按复制updateJson地址
val clipData = ClipData.newPlainText(
"Update JSON URL",
module.updateJson
@ -1163,7 +1152,6 @@ fun ModuleItem(
clipboardManager.setPrimaryClip(clipData)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
// 显示复制成功的提示
Toast.makeText(
context,
context.getString(R.string.module_update_json_copied),
@ -1202,8 +1190,8 @@ fun ModuleItem(
maxLines = 4,
textDecoration = textDecoration,
)
if (!isHideTagRow) {
if (!isHideTagRow) {
Spacer(modifier = Modifier.height(12.dp))
// 文件夹名称和大小标签
Row(
@ -1276,8 +1264,7 @@ fun ModuleItem(
onClick = { onClick(module) },
interactionSource = interactionSource,
contentPadding = ButtonDefaults.TextButtonContentPadding,
) {
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,

View file

@ -2,6 +2,7 @@ package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
@ -10,12 +11,18 @@ import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.rounded.EnhancedEncryption
import androidx.compose.material.icons.rounded.FolderDelete
import androidx.compose.material.icons.rounded.RemoveCircle
import androidx.compose.material.icons.rounded.RemoveModerator
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@ -36,6 +43,8 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination
import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.BuildConfig
@ -46,12 +55,8 @@ import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.setUidAutoScan
import com.sukisu.ultra.ui.util.setUidMultiUserScan
import com.topjohnwu.superuser.ShellUtils
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -60,7 +65,7 @@ import java.time.format.DateTimeFormatter
/**
* @author ShirkNeko
* @date 2025/5/31.
* @date 2025/9/29.
*/
private val SPACING_SMALL = 3.dp
private val SPACING_MEDIUM = 8.dp
@ -81,7 +86,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
Scaffold(
// containerColor = MaterialTheme.colorScheme.surfaceBright,
topBar = {
TopBar(scrollBehavior = scrollBehavior)
},
@ -132,41 +136,155 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
// 卸载模块开关
var umountChecked by rememberSaveable {
mutableStateOf(Natives.isDefaultUmountModules())
val modeItems = listOf(
stringResource(id = R.string.settings_mode_default),
stringResource(id = R.string.settings_mode_temp_enable),
stringResource(id = R.string.settings_mode_always_enable),
)
var enhancedSecurityMode by rememberSaveable {
mutableIntStateOf(
run {
val currentEnabled = Natives.isEnhancedSecurityEnabled()
val savedPersist = prefs.getInt("enhanced_security_mode", 0)
if (savedPersist == 2) 2 else if (currentEnabled) 1 else 0
}
)
}
SuperDropdown(
icon = Icons.Rounded.EnhancedEncryption,
title = stringResource(id = R.string.settings_enable_enhanced_security),
summary = stringResource(id = R.string.settings_enable_enhanced_security_summary),
items = modeItems,
selectedIndex = enhancedSecurityMode,
onSelectedIndexChange = { index ->
when (index) {
// Default: disable and save to persist
0 -> if (Natives.setEnhancedSecurityEnabled(false)) {
execKsud("feature save", true)
prefs.edit { putInt("enhanced_security_mode", 0) }
enhancedSecurityMode = 0
}
SwitchItem(
icon = Icons.Filled.FolderDelete,
title = stringResource(R.string.settings_umount_modules_default),
summary = stringResource(R.string.settings_umount_modules_default_summary),
checked = umountChecked,
onCheckedChange = { enabled ->
if (Natives.setDefaultUmountModules(enabled)) {
umountChecked = enabled
// Temporarily enable: save disabled state first, then enable
1 -> if (Natives.setEnhancedSecurityEnabled(false)) {
execKsud("feature save", true)
if (Natives.setEnhancedSecurityEnabled(true)) {
prefs.edit { putInt("enhanced_security_mode", 0) }
enhancedSecurityMode = 1
}
}
// Permanently enable: enable and save
2 -> if (Natives.setEnhancedSecurityEnabled(true)) {
execKsud("feature save", true)
prefs.edit { putInt("enhanced_security_mode", 2) }
enhancedSecurityMode = 2
}
}
}
)
// SU 禁用开关
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled())
}
SwitchItem(
icon = Icons.Filled.RemoveModerator,
title = stringResource(R.string.settings_disable_su),
summary = stringResource(R.string.settings_disable_su_summary),
checked = isSuDisabled,
onCheckedChange = { enabled ->
val shouldEnable = !enabled
if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = enabled
}
var suCompatMode by rememberSaveable {
mutableIntStateOf(
run {
val currentEnabled = Natives.isSuEnabled()
val savedPersist = prefs.getInt("su_compat_mode", 0)
if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0
}
)
}
SuperDropdown(
icon = Icons.Rounded.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary),
items = modeItems,
selectedIndex = suCompatMode,
onSelectedIndexChange = { index ->
when (index) {
// Default: enable and save to persist
0 -> if (Natives.setSuEnabled(true)) {
execKsud("feature save", true)
prefs.edit { putInt("su_compat_mode", 0) }
suCompatMode = 0
}
// Temporarily disable: save enabled state first, then disable
1 -> if (Natives.setSuEnabled(true)) {
execKsud("feature save", true)
if (Natives.setSuEnabled(false)) {
prefs.edit { putInt("su_compat_mode", 0) }
suCompatMode = 1
}
}
// Permanently disable: disable and save
2 -> if (Natives.setSuEnabled(false)) {
execKsud("feature save", true)
prefs.edit { putInt("su_compat_mode", 2) }
suCompatMode = 2
}
}
}
)
var kernelUmountMode by rememberSaveable {
mutableIntStateOf(
run {
val currentEnabled = Natives.isKernelUmountEnabled()
val savedPersist = prefs.getInt("kernel_umount_mode", 0)
if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0
}
)
}
SuperDropdown(
icon = Icons.Rounded.RemoveCircle,
title = stringResource(id = R.string.settings_disable_kernel_umount),
summary = stringResource(id = R.string.settings_disable_kernel_umount_summary),
items = modeItems,
selectedIndex = kernelUmountMode,
onSelectedIndexChange = { index ->
when (index) {
// Default: enable and save to persist
0 -> if (Natives.setKernelUmountEnabled(true)) {
execKsud("feature save", true)
prefs.edit { putInt("kernel_umount_mode", 0) }
kernelUmountMode = 0
}
// Temporarily disable: save enabled state first, then disable
1 -> if (Natives.setKernelUmountEnabled(true)) {
execKsud("feature save", true)
if (Natives.setKernelUmountEnabled(false)) {
prefs.edit { putInt("kernel_umount_mode", 0) }
kernelUmountMode = 1
}
}
// Permanently disable: disable and save
2 -> if (Natives.setKernelUmountEnabled(false)) {
execKsud("feature save", true)
prefs.edit { putInt("kernel_umount_mode", 2) }
kernelUmountMode = 2
}
}
}
)
// 卸载模块开关
var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) }
SwitchItem(
icon = Icons.Rounded.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked,
onCheckedChange = {
if (Natives.setDefaultUmountModules(it)) {
umountChecked = it
}
}
)
// 强制签名验证开关
var forceSignatureVerification by rememberSaveable {
mutableStateOf(prefs.getBoolean("force_signature_verification", false))
@ -181,115 +299,9 @@ fun SettingScreen(navigator: DestinationsNavigator) {
forceSignatureVerification = enabled
}
)
if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER) {
var uidAutoScanEnabled by rememberSaveable {
mutableStateOf(prefs.getBoolean("uid_auto_scan", false))
}
var uidMultiUserScanEnabled by rememberSaveable {
mutableStateOf(prefs.getBoolean("uid_multi_user_scan", false))
}
// 用户态扫描应用列表开关
SwitchItem(
icon = Icons.Filled.Scanner,
title = stringResource(R.string.uid_auto_scan_title),
summary = stringResource(R.string.uid_auto_scan_summary),
checked = uidAutoScanEnabled,
onCheckedChange = { enabled ->
scope.launch {
try {
if (setUidAutoScan(enabled)) {
uidAutoScanEnabled = enabled
prefs.edit { putBoolean("uid_auto_scan", enabled) }
if (!enabled) {
uidMultiUserScanEnabled = false
prefs.edit { putBoolean("uid_multi_user_scan", false) }
}
} else {
snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed))
}
} catch (e: Exception) {
snackBarHost.showSnackbar(
context.getString(
R.string.uid_scanner_setting_error,
e.message ?: ""
)
)
}
}
}
)
// 多用户应用扫描开关 - 仅在启用用户态扫描时显示
AnimatedVisibility(
visible = uidAutoScanEnabled,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SwitchItem(
icon = Icons.Filled.Groups,
title = stringResource(R.string.uid_multi_user_scan_title),
summary = stringResource(R.string.uid_multi_user_scan_summary),
checked = uidMultiUserScanEnabled,
onCheckedChange = { enabled ->
scope.launch {
try {
if (setUidMultiUserScan(enabled)) {
uidMultiUserScanEnabled = enabled
prefs.edit { putBoolean("uid_multi_user_scan", enabled) }
} else {
snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed))
}
} catch (e: Exception) {
snackBarHost.showSnackbar(
context.getString(
R.string.uid_scanner_setting_error,
e.message ?: ""
)
)
}
}
}
)
}
// 清理运行环境
AnimatedVisibility(
visible = uidAutoScanEnabled,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
val confirmDialog = rememberConfirmDialog()
val scope = rememberCoroutineScope()
SettingItem(
icon = Icons.Filled.CleaningServices,
title = stringResource(R.string.clean_runtime_environment),
summary = stringResource(R.string.clean_runtime_environment_summary),
onClick = {
scope.launch {
val result = confirmDialog.awaitConfirm(
title = context.getString(R.string.clean_runtime_environment),
content = context.getString(R.string.clean_runtime_environment_confirm)
)
if (result == ConfirmResult.Confirmed) {
val cleanResult = cleanRuntimeEnvironment()
if (cleanResult) {
uidAutoScanEnabled = false
prefs.edit { putBoolean("uid_auto_scan", false) }
uidMultiUserScanEnabled = false
prefs.edit { putBoolean("uid_multi_user_scan", false) }
snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_success))
} else {
snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_failed))
}
}
}
}
)
}
// UID 扫描开关
if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) {
UidScannerSection(prefs, snackBarHost, scope, context)
}
}
)
@ -389,6 +401,31 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
// 查看使用日志
KsuIsValid {
SettingItem(
icon = Icons.Filled.Visibility,
title = stringResource(R.string.log_viewer_view_logs),
summary = stringResource(R.string.log_viewer_view_logs_summary),
onClick = {
navigator.navigate(LogViewerScreenDestination)
}
)
}
val lkmMode = Natives.isLkmMode
KsuIsValid {
if (lkmMode) {
SettingItem(
icon = Icons.Filled.FolderOff,
title = stringResource(R.string.umount_path_manager),
summary = stringResource(R.string.umount_path_manager_summary),
onClick = {
navigator.navigate(UmountManagerScreenDestination)
}
)
}
}
if (showBottomsheet) {
LogBottomSheet(
onDismiss = { showBottomsheet = false },
@ -430,8 +467,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
)
}
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) {
UninstallItem(navigator) {
loadingDialog.withLoading(it)
@ -459,23 +494,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
}
fun cleanRuntimeEnvironment(): Boolean {
val shell = getRootShell()
return try {
try {
ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop")
} catch (_: Exception) {
}
ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh")
true
} catch (_: Exception) {
false
}
}
@Composable
private fun SettingsGroupCard(
title: String,
@ -781,7 +799,6 @@ enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val options = listOf(
// UninstallType.TEMPORARY,
UninstallType.PERMANENT,
UninstallType.RESTORE_STOCK_IMAGE
)
@ -938,4 +955,125 @@ private fun TopBar(
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
}
@Composable
private fun UidScannerSection(
prefs: SharedPreferences,
snackBarHost: SnackbarHostState,
scope: CoroutineScope,
context: Context
) {
if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return
val realAuto = Natives.isUidScannerEnabled()
val realMulti = getUidMultiUserScan()
var autoOn by remember { mutableStateOf(realAuto) }
var multiOn by remember { mutableStateOf(realMulti) }
LaunchedEffect(Unit) {
autoOn = realAuto
multiOn = realMulti
prefs.edit {
putBoolean("uid_auto_scan", autoOn)
putBoolean("uid_multi_user_scan", multiOn)
}
}
SwitchItem(
icon = Icons.Filled.Scanner,
title = stringResource(R.string.uid_auto_scan_title),
summary = stringResource(R.string.uid_auto_scan_summary),
checked = autoOn,
onCheckedChange = { target ->
autoOn = target
if (!target) multiOn = false
scope.launch(Dispatchers.IO) {
setUidAutoScan(target)
val actual = Natives.isUidScannerEnabled() || readUidScannerFile()
withContext(Dispatchers.Main) {
autoOn = actual
if (!actual) multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", actual)
putBoolean("uid_multi_user_scan", multiOn)
}
if (actual != target) {
snackBarHost.showSnackbar(
context.getString(R.string.uid_scanner_setting_failed)
)
}
}
}
}
)
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SwitchItem(
icon = Icons.Filled.Groups,
title = stringResource(R.string.uid_multi_user_scan_title),
summary = stringResource(R.string.uid_multi_user_scan_summary),
checked = multiOn,
onCheckedChange = { target ->
scope.launch(Dispatchers.IO) {
val ok = setUidMultiUserScan(target)
withContext(Dispatchers.Main) {
if (ok) {
multiOn = target
prefs.edit { putBoolean("uid_multi_user_scan", target) }
} else {
snackBarHost.showSnackbar(
context.getString(R.string.uid_scanner_setting_failed)
)
}
}
}
}
)
}
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
val confirmDialog = rememberConfirmDialog()
SettingItem(
icon = Icons.Filled.CleaningServices,
title = stringResource(R.string.clean_runtime_environment),
summary = stringResource(R.string.clean_runtime_environment_summary),
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.clean_runtime_environment),
content = context.getString(R.string.clean_runtime_environment_confirm)
) == ConfirmResult.Confirmed
) {
if (cleanRuntimeEnvironment()) {
autoOn = false
multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", false)
putBoolean("uid_multi_user_scan", false)
}
Natives.setUidScannerEnabled(false)
snackBarHost.showSnackbar(
context.getString(R.string.clean_runtime_environment_success)
)
} else {
snackBarHost.showSnackbar(
context.getString(R.string.clean_runtime_environment_failed)
)
}
}
}
}
)
}
}

View file

@ -3,10 +3,12 @@ package com.sukisu.ultra.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -17,10 +19,13 @@ import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.getSystemService
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.lifecycle.viewmodel.compose.viewModel
@ -253,4 +258,25 @@ private fun TopBar(
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
}
@Composable
fun LabelText(label: String) {
Box(
modifier = Modifier
.padding(top = 4.dp, end = 4.dp)
.background(
Color.Black,
shape = RoundedCornerShape(4.dp)
)
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
style = TextStyle(
fontSize = 8.sp,
color = Color.White,
)
)
}
}

View file

@ -0,0 +1,442 @@
package com.sukisu.ultra.ui.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private val SPACING_SMALL = 3.dp
private val SPACING_MEDIUM = 8.dp
private val SPACING_LARGE = 16.dp
data class UmountPathEntry(
val path: String,
val checkMnt: Boolean,
val flags: Int,
val isDefault: Boolean
)
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun UmountManagerScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
val confirmDialog = rememberConfirmDialog()
var pathList by remember { mutableStateOf<List<UmountPathEntry>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var showAddDialog by remember { mutableStateOf(false) }
fun loadPaths() {
scope.launch(Dispatchers.IO) {
isLoading = true
val result = listUmountPaths()
val entries = parseUmountPaths(result)
withContext(Dispatchers.Main) {
pathList = entries
isLoading = false
}
}
}
LaunchedEffect(Unit) {
loadPaths()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.umount_path_manager)) },
navigationIcon = {
IconButton(onClick = { navigator.navigateUp() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
actions = {
IconButton(onClick = { loadPaths() }) {
Icon(Icons.Filled.Refresh, contentDescription = null)
}
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(
alpha = CardConfig.cardAlpha
)
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showAddDialog = true }
) {
Icon(Icons.Filled.Add, contentDescription = null)
}
},
snackbarHost = { SnackbarHost(snackBarHost) }
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
colors = getCardColors(MaterialTheme.colorScheme.primaryContainer),
elevation = getCardElevation()
) {
Column(
modifier = Modifier.padding(SPACING_LARGE)
) {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Text(
text = stringResource(R.string.umount_path_restart_notice),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
items(pathList, key = { it.path }) { entry ->
UmountPathCard(
entry = entry,
onDelete = {
scope.launch(Dispatchers.IO) {
val success = removeUmountPath(entry.path)
withContext(Dispatchers.Main) {
if (success) {
snackBarHost.showSnackbar(
context.getString(R.string.umount_path_removed)
)
loadPaths()
} else {
snackBarHost.showSnackbar(
context.getString(R.string.operation_failed)
)
}
}
}
}
)
}
item {
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE),
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
Button(
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.confirm_action),
content = context.getString(R.string.confirm_clear_custom_paths)
) == ConfirmResult.Confirmed) {
withContext(Dispatchers.IO) {
val success = clearCustomUmountPaths()
withContext(Dispatchers.Main) {
if (success) {
snackBarHost.showSnackbar(
context.getString(R.string.custom_paths_cleared)
)
loadPaths()
} else {
snackBarHost.showSnackbar(
context.getString(R.string.operation_failed)
)
}
}
}
}
}
},
modifier = Modifier.weight(1f)
) {
Icon(Icons.Filled.DeleteForever, contentDescription = null)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(stringResource(R.string.clear_custom_paths))
}
Button(
onClick = {
scope.launch(Dispatchers.IO) {
val success = applyUmountConfigToKernel()
withContext(Dispatchers.Main) {
if (success) {
snackBarHost.showSnackbar(
context.getString(R.string.config_applied)
)
} else {
snackBarHost.showSnackbar(
context.getString(R.string.operation_failed)
)
}
}
}
},
modifier = Modifier.weight(1f)
) {
Icon(Icons.Filled.Check, contentDescription = null)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(stringResource(R.string.apply_config))
}
}
}
}
}
}
if (showAddDialog) {
AddUmountPathDialog(
onDismiss = { showAddDialog = false },
onConfirm = { path, checkMnt, flags ->
showAddDialog = false
scope.launch(Dispatchers.IO) {
val success = addUmountPath(path, checkMnt, flags)
withContext(Dispatchers.Main) {
if (success) {
saveUmountConfig()
snackBarHost.showSnackbar(
context.getString(R.string.umount_path_added)
)
loadPaths()
} else {
snackBarHost.showSnackbar(
context.getString(R.string.operation_failed)
)
}
}
}
}
)
}
}
}
@Composable
fun UmountPathCard(
entry: UmountPathEntry,
onDelete: () -> Unit
) {
val confirmDialog = rememberConfirmDialog()
val scope = rememberCoroutineScope()
val context = LocalContext.current
Card(
modifier = Modifier.fillMaxWidth(),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = getCardElevation()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Folder,
contentDescription = null,
tint = if (entry.isDefault)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(SPACING_LARGE))
Column(modifier = Modifier.weight(1f)) {
Text(
text = entry.path,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = buildString {
append(context.getString(R.string.check_mount_type))
append(": ")
append(if (entry.checkMnt) context.getString(R.string.yes) else context.getString(R.string.no))
append(" | ")
append(context.getString(R.string.flags))
append(": ")
append(entry.flags.toUmountFlagName(context))
if (entry.isDefault) {
append(" | ")
append(context.getString(R.string.default_entry))
}
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (!entry.isDefault) {
IconButton(
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.confirm_delete),
content = context.getString(R.string.confirm_delete_umount_path, entry.path)
) == ConfirmResult.Confirmed) {
onDelete()
}
}
}
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}
@Composable
fun AddUmountPathDialog(
onDismiss: () -> Unit,
onConfirm: (String, Boolean, Int) -> Unit
) {
var path by rememberSaveable { mutableStateOf("") }
var checkMnt by rememberSaveable { mutableStateOf(false) }
var flags by rememberSaveable { mutableStateOf("-1") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.add_umount_path)) },
text = {
Column {
OutlinedTextField(
value = path,
onValueChange = { path = it },
label = { Text(stringResource(R.string.mount_path)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = checkMnt,
onCheckedChange = { checkMnt = it }
)
Spacer(modifier = Modifier.width(SPACING_SMALL))
Text(stringResource(R.string.check_mount_type_overlay))
}
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
OutlinedTextField(
value = flags,
onValueChange = { flags = it },
label = { Text(stringResource(R.string.umount_flags)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText = { Text(stringResource(R.string.umount_flags_hint)) }
)
}
},
confirmButton = {
TextButton(
onClick = {
val flagsInt = flags.toIntOrNull() ?: -1
onConfirm(path, checkMnt, flagsInt)
},
enabled = path.isNotBlank()
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
private fun parseUmountPaths(output: String): List<UmountPathEntry> {
val lines = output.lines().filter { it.isNotBlank() }
if (lines.size < 2) return emptyList()
return lines.drop(2).mapNotNull { line ->
val parts = line.trim().split(Regex("\\s+"))
if (parts.size >= 4) {
UmountPathEntry(
path = parts[0],
checkMnt = parts[1].equals("true", ignoreCase = true),
flags = parts[2].toIntOrNull() ?: -1,
isDefault = parts[3].equals("Yes", ignoreCase = true)
)
} else null
}
}
private fun Int.toUmountFlagName(context: android.content.Context): String {
return when (this) {
-1 -> context.getString(R.string.mnt_detach)
else -> this.toString()
}
}

View file

@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.screen
package com.sukisu.ultra.ui.susfs
import android.annotation.SuppressLint
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
@ -25,11 +26,13 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.susfs.component.*
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion159
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion1512
import com.sukisu.ultra.ui.util.getSuSFSVersion
import com.sukisu.ultra.ui.util.isAbDevice
import kotlinx.coroutines.launch
import java.io.File
@ -43,6 +46,7 @@ enum class SuSFSTab(val displayNameRes: Int) {
BASIC_SETTINGS(R.string.susfs_tab_basic_settings),
SUS_PATHS(R.string.susfs_tab_sus_paths),
SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths),
SUS_MAPS(R.string.susfs_tab_sus_maps),
SUS_MOUNTS(R.string.susfs_tab_sus_mounts),
TRY_UMOUNT(R.string.susfs_tab_try_umount),
KSTAT_CONFIG(R.string.susfs_tab_kstat_config),
@ -50,11 +54,12 @@ enum class SuSFSTab(val displayNameRes: Int) {
ENABLED_FEATURES(R.string.susfs_tab_enabled_features);
companion object {
fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean): List<SuSFSTab> {
fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean, isSusVersion1512: Boolean): List<SuSFSTab> {
return when {
isSusVersion159 -> entries.toList()
isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS }
else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS }
isSusVersion1512 -> entries.toList()
isSusVersion159 -> entries.filter { it != SUS_MAPS}
isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS && it != SUS_MAPS }
else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS && it != SUS_MAPS }
}
}
}
@ -93,6 +98,7 @@ fun SuSFSConfigScreen(
// 路径管理相关状态
var susPaths by remember { mutableStateOf(emptySet<String>()) }
var susLoopPaths by remember { mutableStateOf(emptySet<String>()) }
var susMaps by remember { mutableStateOf(emptySet<String>()) }
var susMounts by remember { mutableStateOf(emptySet<String>()) }
var tryUmounts by remember { mutableStateOf(emptySet<String>()) }
var androidDataPath by remember { mutableStateOf("") }
@ -117,16 +123,17 @@ fun SuSFSConfigScreen(
// 对话框状态
var showAddPathDialog by remember { mutableStateOf(false) }
var showAddLoopPathDialog by remember { mutableStateOf(false) }
var showAddSusMapDialog by remember { mutableStateOf(false) }
var showAddAppPathDialog by remember { mutableStateOf(false) }
var showAddMountDialog by remember { mutableStateOf(false) }
var showAddUmountDialog by remember { mutableStateOf(false) }
var showRunUmountDialog by remember { mutableStateOf(false) }
var showAddKstatStaticallyDialog by remember { mutableStateOf(false) }
var showAddKstatDialog by remember { mutableStateOf(false) }
// 编辑状态
var editingPath by remember { mutableStateOf<String?>(null) }
var editingLoopPath by remember { mutableStateOf<String?>(null) }
var editingSusMap by remember { mutableStateOf<String?>(null) }
var editingMount by remember { mutableStateOf<String?>(null) }
var editingUmount by remember { mutableStateOf<String?>(null) }
var editingKstatConfig by remember { mutableStateOf<String?>(null) }
@ -135,6 +142,7 @@ fun SuSFSConfigScreen(
// 重置确认对话框状态
var showResetPathsDialog by remember { mutableStateOf(false) }
var showResetLoopPathsDialog by remember { mutableStateOf(false) }
var showResetSusMapsDialog by remember { mutableStateOf(false) }
var showResetMountsDialog by remember { mutableStateOf(false) }
var showResetUmountsDialog by remember { mutableStateOf(false) }
var showResetKstatDialog by remember { mutableStateOf(false) }
@ -148,7 +156,7 @@ fun SuSFSConfigScreen(
var isNavigating by remember { mutableStateOf(false) }
val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159())
val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159(), isSusVersion1512())
// 实时判断是否可以启用开机自启动
val canEnableAutoStart by remember {
@ -157,6 +165,38 @@ fun SuSFSConfigScreen(
}
}
var showVersionMismatchDialog by remember { mutableStateOf(false) }
if (showVersionMismatchDialog) {
AlertDialog(
onDismissRequest = { showVersionMismatchDialog = false },
title = {
Text(
text = stringResource(R.string.warning),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
stringResource(
R.string.susfs_version_mismatch,
try { getSuSFSVersion() } catch (_: Exception) { "unknown" },
SuSFSManager.MAX_SUSFS_VERSION
)
)
},
confirmButton = {
TextButton(
onClick = { showVersionMismatchDialog = false },
modifier = Modifier.padding(8.dp)
) {
Text(stringResource(R.string.confirm))
}
}
)
}
// 文件选择器
val backupFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
@ -242,25 +282,42 @@ fun SuSFSConfigScreen(
// 加载当前配置
LaunchedEffect(Unit) {
unameValue = SuSFSManager.getUnameValue(context)
buildTimeValue = SuSFSManager.getBuildTimeValue(context)
autoStartEnabled = SuSFSManager.isAutoStartEnabled(context)
executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context)
susPaths = SuSFSManager.getSusPaths(context)
susLoopPaths = SuSFSManager.getSusLoopPaths(context)
susMounts = SuSFSManager.getSusMounts(context)
tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context)
sdcardPath = SuSFSManager.getSdcardPath(context)
kstatConfigs = SuSFSManager.getKstatConfigs(context)
addKstatPaths = SuSFSManager.getAddKstatPaths(context)
hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context)
enableHideBl = SuSFSManager.getEnableHideBl(context)
enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context)
umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context)
enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context)
coroutineScope.launch {
try {
val version = getSuSFSVersion()
val binaryName = "ksu_susfs_${version.removePrefix("v")}"
loadSlotInfo()
val isBinaryAvailable = try {
context.assets.open(binaryName).use { true }
} catch (_: Exception) { false }
if (!isBinaryAvailable) {
showVersionMismatchDialog = true
}
} catch (_: Exception) {
}
unameValue = SuSFSManager.getUnameValue(context)
buildTimeValue = SuSFSManager.getBuildTimeValue(context)
autoStartEnabled = SuSFSManager.isAutoStartEnabled(context)
executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context)
susPaths = SuSFSManager.getSusPaths(context)
susLoopPaths = SuSFSManager.getSusLoopPaths(context)
susMaps = SuSFSManager.getSusMaps(context)
susMounts = SuSFSManager.getSusMounts(context)
tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context)
sdcardPath = SuSFSManager.getSdcardPath(context)
kstatConfigs = SuSFSManager.getKstatConfigs(context)
addKstatPaths = SuSFSManager.getAddKstatPaths(context)
hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context)
enableHideBl = SuSFSManager.getEnableHideBl(context)
enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context)
umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context)
enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context)
loadSlotInfo()
}
}
// 当切换到启用功能状态标签页时加载数据
@ -419,6 +476,7 @@ fun SuSFSConfigScreen(
executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context)
susPaths = SuSFSManager.getSusPaths(context)
susLoopPaths = SuSFSManager.getSusLoopPaths(context)
susMaps = SuSFSManager.getSusMaps(context)
susMounts = SuSFSManager.getSusMounts(context)
tryUmounts = SuSFSManager.getTryUmounts(context)
androidDataPath = SuSFSManager.getAndroidDataPath(context)
@ -537,6 +595,35 @@ fun SuSFSConfigScreen(
initialValue = editingLoopPath ?: ""
)
AddPathDialog(
showDialog = showAddSusMapDialog,
onDismiss = {
showAddSusMapDialog = false
editingSusMap = null
},
onConfirm = { path ->
coroutineScope.launch {
isLoading = true
val success = if (editingSusMap != null) {
SuSFSManager.editSusMap(context, editingSusMap!!, path)
} else {
SuSFSManager.addSusMap(context, path)
}
if (success) {
susMaps = SuSFSManager.getSusMaps(context)
}
isLoading = false
showAddSusMapDialog = false
editingSusMap = null
}
},
isLoading = isLoading,
titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map,
labelRes = R.string.susfs_sus_map_label,
placeholderRes = R.string.susfs_sus_map_placeholder,
initialValue = editingSusMap ?: ""
)
AddAppPathDialog(
showDialog = showAddAppPathDialog,
onDismiss = { showAddAppPathDialog = false },
@ -629,8 +716,21 @@ fun SuSFSConfigScreen(
isLoading = true
val success = if (editingKstatConfig != null) {
SuSFSManager.editKstatConfig(
context, editingKstatConfig!!, path, ino, dev, nlink, size, atime, atimeNsec,
mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize
context,
editingKstatConfig!!,
path,
ino,
dev,
nlink,
size,
atime,
atimeNsec,
mtime,
mtimeNsec,
ctime,
ctimeNsec,
blocks,
blksize
)
} else {
SuSFSManager.addKstatStatically(
@ -680,22 +780,6 @@ fun SuSFSConfigScreen(
)
// 确认对话框
ConfirmDialog(
showDialog = showRunUmountDialog,
onDismiss = { showRunUmountDialog = false },
onConfirm = {
coroutineScope.launch {
isLoading = true
SuSFSManager.runTryUmount(context)
isLoading = false
showRunUmountDialog = false
}
},
titleRes = R.string.susfs_run_umount_confirm_title,
messageRes = R.string.susfs_run_umount_confirm_message,
isLoading = isLoading
)
ConfirmDialog(
showDialog = showConfirmReset,
onDismiss = { showConfirmReset = false },
@ -760,6 +844,27 @@ fun SuSFSConfigScreen(
isDestructive = true
)
ConfirmDialog(
showDialog = showResetSusMapsDialog,
onDismiss = { showResetSusMapsDialog = false },
onConfirm = {
coroutineScope.launch {
isLoading = true
SuSFSManager.saveSusMaps(context, emptySet())
susMaps = emptySet()
if (SuSFSManager.isAutoStartEnabled(context)) {
SuSFSManager.configureAutoStart(context, true)
}
isLoading = false
showResetSusMapsDialog = false
}
},
titleRes = R.string.susfs_reset_sus_maps_title,
messageRes = R.string.susfs_reset_sus_maps_message,
isLoading = isLoading,
isDestructive = true
)
ConfirmDialog(
showDialog = showResetMountsDialog,
onDismiss = { showResetMountsDialog = false },
@ -979,6 +1084,28 @@ fun SuSFSConfigScreen(
}
}
SuSFSTab.SUS_MAPS -> {
OutlinedButton(
onClick = { showResetSusMapsDialog = true },
enabled = !isLoading && susMaps.isNotEmpty(),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
) {
Icon(
imageVector = Icons.Default.RestoreFromTrash,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
stringResource(R.string.susfs_reset_sus_maps_title),
fontWeight = FontWeight.Medium
)
}
}
SuSFSTab.SUS_MOUNTS -> {
OutlinedButton(
onClick = { showResetMountsDialog = true },
@ -1110,12 +1237,12 @@ fun SuSFSConfigScreen(
.padding(horizontal = 12.dp)
) {
// 标签页
ScrollableTabRow(
PrimaryScrollableTabRow(
selectedTabIndex = allTabs.indexOf(selectedTab),
edgePadding = 0.dp,
modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface
contentColor = MaterialTheme.colorScheme.onSurface,
edgePadding = 0.dp
) {
allTabs.forEach { tab ->
Tab(
@ -1243,6 +1370,26 @@ fun SuSFSConfigScreen(
}
)
}
SuSFSTab.SUS_MAPS -> {
SusMapsContent(
susMaps = susMaps,
isLoading = isLoading,
onAddSusMap = { showAddSusMapDialog = true },
onRemoveSusMap = { map ->
coroutineScope.launch {
isLoading = true
if (SuSFSManager.removeSusMap(context, map)) {
susMaps = SuSFSManager.getSusMaps(context)
}
isLoading = false
}
},
onEditSusMap = { map ->
editingSusMap = map
showAddSusMapDialog = true
}
)
}
SuSFSTab.SUS_MOUNTS -> {
val isSusVersion158 = remember { isSusVersion158() }
@ -1268,7 +1415,11 @@ fun SuSFSConfigScreen(
onToggleHideSusMountsForAllProcs = { hideForAll ->
coroutineScope.launch {
isLoading = true
if (SuSFSManager.setHideSusMountsForAllProcs(context, hideForAll)) {
if (SuSFSManager.setHideSusMountsForAllProcs(
context,
hideForAll
)
) {
hideSusMountsForAllProcs = hideForAll
}
isLoading = false
@ -1283,7 +1434,6 @@ fun SuSFSConfigScreen(
umountForZygoteIsoService = umountForZygoteIsoService,
isLoading = isLoading,
onAddUmount = { showAddUmountDialog = true },
onRunUmount = { showRunUmountDialog = true },
onRemoveUmount = { umountEntry ->
coroutineScope.launch {
isLoading = true
@ -1300,7 +1450,8 @@ fun SuSFSConfigScreen(
onToggleUmountForZygoteIsoService = { enabled ->
coroutineScope.launch {
isLoading = true
val success = SuSFSManager.setUmountForZygoteIsoService(context, enabled)
val success =
SuSFSManager.setUmountForZygoteIsoService(context, enabled)
if (success) {
umountForZygoteIsoService = enabled
}
@ -1411,7 +1562,7 @@ private fun BasicSettingsContent(
isLoading: Boolean,
onAutoStartToggle: (Boolean) -> Unit,
onShowSlotInfo: () -> Unit,
context: android.content.Context,
context: Context,
onShowBackupDialog: () -> Unit,
onShowRestoreDialog: () -> Unit,
enableHideBl: Boolean,
@ -1422,7 +1573,9 @@ private fun BasicSettingsContent(
onEnableAvcLogSpoofingChange: (Boolean) -> Unit
) {
var scriptLocationExpanded by remember { mutableStateOf(false) }
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
val isSusVersion159 = isSusVersion159()
Column(
@ -1498,7 +1651,7 @@ private fun BasicSettingsContent(
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = scriptLocationExpanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryEditable, true),
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true),
shape = RoundedCornerShape(8.dp),
enabled = !isLoading
)
@ -1912,7 +2065,9 @@ private fun SlotInfoDialog(
onUseUname: (String) -> Unit,
onUseBuildTime: (String) -> Unit
) {
val isAbDevice = isAbDevice()
val isAbDevice = produceState(initialValue = false) {
value = isAbDevice()
}.value
if (showDialog && isAbDevice) {
AlertDialog(

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package com.sukisu.ultra.ui.susfs.component
import android.annotation.SuppressLint
import android.content.pm.PackageInfo
@ -29,7 +29,7 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import kotlinx.coroutines.launch
@ -464,7 +464,7 @@ fun AddTryUmountDialog(
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryEditable, true),
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true),
shape = RoundedCornerShape(8.dp)
)
ExposedDropdownMenu(

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package com.sukisu.ultra.ui.susfs.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
@ -18,8 +18,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.susfs.util.SuSFSManager
import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
/**
@ -310,6 +310,116 @@ fun SusLoopPathsContent(
}
}
/**
* SUS Maps内容组件
*/
@Composable
fun SusMapsContent(
susMaps: Set<String>,
isLoading: Boolean,
onAddSusMap: () -> Unit,
onRemoveSusMap: (String) -> Unit,
onEditSusMap: ((String) -> Unit)? = null
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 说明卡片
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.sus_maps_description_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(R.string.sus_maps_description_text),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.sus_maps_warning),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = stringResource(R.string.sus_maps_debug_info),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
}
}
if (susMaps.isEmpty()) {
item {
EmptyStateCard(
message = stringResource(R.string.susfs_no_sus_maps_configured)
)
}
} else {
item {
SectionHeader(
title = stringResource(R.string.sus_maps_section),
subtitle = null,
icon = Icons.Default.Security,
count = susMaps.size
)
}
items(susMaps.toList()) { map ->
PathItemCard(
path = map,
icon = Icons.Default.Security,
onDelete = { onRemoveSusMap(map) },
onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null,
isLoading = isLoading
)
}
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = onAddSusMap,
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(8.dp)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(R.string.add))
}
}
}
}
}
}
/**
* SUS挂载内容组件
*/
@ -395,7 +505,6 @@ fun TryUmountContent(
umountForZygoteIsoService: Boolean,
isLoading: Boolean,
onAddUmount: () -> Unit,
onRunUmount: () -> Unit,
onRemoveUmount: (String) -> Unit,
onEditUmount: ((String) -> Unit)? = null,
onToggleUmountForZygoteIsoService: (Boolean) -> Unit
@ -509,24 +618,6 @@ fun TryUmountContent(
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(R.string.add))
}
if (tryUmounts.isNotEmpty()) {
Button(
onClick = onRunUmount,
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(8.dp)
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(R.string.susfs_run))
}
}
}
}
}

View file

@ -1,14 +1,14 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.susfs.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Build
import android.util.Log
import android.widget.Toast
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
@ -19,9 +19,14 @@ import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import androidx.core.content.edit
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.getSuSFSVersion
import com.sukisu.ultra.ui.util.getSuSFSFeatures
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
@ -37,6 +42,8 @@ object SuSFSManager {
private const val KEY_AUTO_START_ENABLED = "auto_start_enabled"
private const val KEY_SUS_PATHS = "sus_paths"
private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths"
private const val KEY_SUS_MAPS = "sus_maps"
private const val KEY_SUS_MOUNTS = "sus_mounts"
private const val KEY_TRY_UMOUNTS = "try_umounts"
private const val KEY_ANDROID_DATA_PATH = "android_data_path"
@ -60,6 +67,8 @@ object SuSFSManager {
private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID"
private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8"
private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9"
private const val MIN_VERSION_SUS_MAPS = "1.5.12"
const val MAX_SUSFS_VERSION = "2.0.0"
private const val BACKUP_FILE_EXTENSION = ".susfs_backup"
private const val MEDIA_DATA_PATH = "/data/media/0/Android/data"
private const val CGROUP_UID_PATH_PREFIX = "/sys/fs/cgroup/uid_"
@ -112,7 +121,7 @@ object SuSFSManager {
configurationsJson.keys().forEach { key ->
val value = configurationsJson.get(key)
configurations[key] = when (value) {
is org.json.JSONArray -> {
is JSONArray -> {
val set = mutableSetOf<String>()
for (i in 0 until value.length()) {
set.add(value.getString(i))
@ -147,6 +156,7 @@ object SuSFSManager {
val executeInPostFsData: Boolean,
val susPaths: Set<String>,
val susLoopPaths: Set<String>,
val susMaps: Set<String>,
val susMounts: Set<String>,
val tryUmounts: Set<String>,
val androidDataPath: String,
@ -169,6 +179,7 @@ object SuSFSManager {
buildTimeValue != DEFAULT_BUILD_TIME ||
susPaths.isNotEmpty() ||
susLoopPaths.isNotEmpty() ||
susMaps.isNotEmpty() ||
susMounts.isNotEmpty() ||
tryUmounts.isNotEmpty() ||
kstatConfigs.isNotEmpty() ||
@ -180,11 +191,23 @@ object SuSFSManager {
private fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private fun getSuSFSVersionUse(): String = try {
getSuSFSVersion()
} catch (_: Exception) { MIN_VERSION_FOR_HIDE_MOUNT }
private fun getSuSFSVersionUse(context: Context): String = try {
val version = getSuSFSVersion()
val binaryName = "${SUSFS_BINARY_TARGET_NAME}_${version.removePrefix("v")}"
if (isBinaryAvailable(context, binaryName)) {
version
} else {
MAX_SUSFS_VERSION
}
} catch (_: Exception) {
MAX_SUSFS_VERSION
}
private fun getSuSFSBinaryName(): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse().removePrefix("v")}"
fun isBinaryAvailable(context: Context, binaryName: String): Boolean = try {
context.assets.open(binaryName).use { true }
} catch (_: IOException) { false }
private fun getSuSFSBinaryName(context: Context): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse(context).removePrefix("v")}"
private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/$SUSFS_BINARY_TARGET_NAME"
@ -222,29 +245,19 @@ object SuSFSManager {
return 0
}
/**
* 检查是否支持设置sdcard路径等功能1.5.8+
*/
fun isSusVersion158(): Boolean {
return try {
val currentVersion = getSuSFSVersion()
compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0
} catch (_: Exception) {
true
}
private fun isVersionAtLeast(minVersion: String): Boolean = try {
compareVersions(getSuSFSVersion(), minVersion) >= 0
} catch (_: Exception) {
true
}
// 检查是否支持设置sdcard路径等功能1.5.8+
fun isSusVersion158(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_HIDE_MOUNT)
/**
* 检查是否支持循环路径和AVC日志欺骗等功能1.5.9+
*/
fun isSusVersion159(): Boolean {
return try {
val currentVersion = getSuSFSVersion()
compareVersions(currentVersion, MIN_VERSION_FOR_LOOP_PATH) >= 0
} catch (_: Exception) {
true
}
}
// 检查是否支持循环路径和AVC日志欺骗等功能1.5.9+
fun isSusVersion159(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_LOOP_PATH)
// 检查是否支持隐藏内存映射1.5.12+
fun isSusVersion1512(): Boolean = isVersionAtLeast(MIN_VERSION_SUS_MAPS)
/**
* 获取当前模块配置
@ -257,6 +270,7 @@ object SuSFSManager {
executeInPostFsData = getExecuteInPostFsData(context),
susPaths = getSusPaths(context),
susLoopPaths = getSusLoopPaths(context),
susMaps = getSusMaps(context),
susMounts = getSusMounts(context),
tryUmounts = getTryUmounts(context),
androidDataPath = getAndroidDataPath(context),
@ -304,7 +318,7 @@ object SuSFSManager {
fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) {
getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) }
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}
@ -361,6 +375,12 @@ object SuSFSManager {
fun getSusLoopPaths(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet()
fun saveSusMaps(context: Context, maps: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) }
fun getSusMaps(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet()
fun saveSusMounts(context: Context, mounts: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) }
@ -526,6 +546,7 @@ object SuSFSManager {
KEY_AUTO_START_ENABLED to isAutoStartEnabled(context),
KEY_SUS_PATHS to getSusPaths(context),
KEY_SUS_LOOP_PATHS to getSusLoopPaths(context),
KEY_SUS_MAPS to getSusMaps(context),
KEY_SUS_MOUNTS to getSusMounts(context),
KEY_TRY_UMOUNTS to getTryUmounts(context),
KEY_ANDROID_DATA_PATH to getAndroidDataPath(context),
@ -552,7 +573,7 @@ object SuSFSManager {
// 获取设备信息
private fun getDeviceInfo(): String {
return try {
"${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.Build.VERSION.RELEASE})"
"${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})"
} catch (_: Exception) {
"Unknown Device"
}
@ -710,7 +731,7 @@ object SuSFSManager {
// 二进制文件管理
private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) {
try {
val binaryName = getSuSFSBinaryName()
val binaryName = getSuSFSBinaryName(context)
val targetPath = getSuSFSTargetPath()
val tempFile = File(context.cacheDir, binaryName)
@ -731,7 +752,7 @@ object SuSFSManager {
}
fun isBinaryAvailable(context: Context): Boolean = try {
context.assets.open(getSuSFSBinaryName()).use { true }
context.assets.open(getSuSFSBinaryName(context)).use { true }
} catch (_: IOException) { false }
// 命令执行
@ -818,9 +839,10 @@ object SuSFSManager {
// 功能状态获取
suspend fun getEnabledFeatures(context: Context): List<EnabledFeature> = withContext(Dispatchers.IO) {
try {
val status = Natives.getSusfsFeatureStatus()
if (status != null) {
parseEnabledFeaturesFromStatus(context, status)
val featuresOutput = getSuSFSFeatures()
if (featuresOutput.isNotBlank() && featuresOutput != "Invalid") {
parseEnabledFeaturesFromOutput(context, featuresOutput)
} else {
getDefaultDisabledFeatures(context)
}
@ -830,10 +852,47 @@ object SuSFSManager {
}
}
private fun parseEnabledFeaturesFromOutput(context: Context, featuresOutput: String): List<EnabledFeature> {
val enabledConfigs = featuresOutput.lines()
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
val featureMap = mapOf(
"CONFIG_KSU_SUSFS_SUS_PATH" to context.getString(R.string.sus_path_feature_label),
"CONFIG_KSU_SUSFS_SUS_MOUNT" to context.getString(R.string.sus_mount_feature_label),
"CONFIG_KSU_SUSFS_TRY_UMOUNT" to context.getString(R.string.try_umount_feature_label),
"CONFIG_KSU_SUSFS_SPOOF_UNAME" to context.getString(R.string.spoof_uname_feature_label),
"CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label),
"CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label),
"CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label),
"CONFIG_KSU_SUSFS_AUTO_ADD_SUS_KSU_DEFAULT_MOUNT" to context.getString(R.string.auto_default_mount_feature_label),
"CONFIG_KSU_SUSFS_AUTO_ADD_SUS_BIND_MOUNT" to context.getString(R.string.auto_bind_mount_feature_label),
"CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label),
"CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label),
"CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label),
"CONFIG_KSU_SUSFS_SUS_SU" to context.getString(R.string.sus_su_feature_label)
)
return featureMap.map { (configKey, displayName) ->
val isEnabled = enabledConfigs.contains(configKey)
val statusText = if (isEnabled) {
context.getString(R.string.susfs_feature_enabled)
} else {
context.getString(R.string.susfs_feature_disabled)
}
val canConfigure = displayName == context.getString(R.string.enable_log_feature_label)
EnabledFeature(displayName, isEnabled, statusText, canConfigure)
}.sortedBy { it.name }
}
private fun getDefaultDisabledFeatures(context: Context): List<EnabledFeature> {
val defaultFeatures = listOf(
"sus_path_feature_label" to context.getString(R.string.sus_path_feature_label),
"sus_loop_path_feature_label" to context.getString(R.string.sus_loop_path_feature_label),
"sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label),
"try_umount_feature_label" to context.getString(R.string.try_umount_feature_label),
"spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label),
@ -845,7 +904,6 @@ object SuSFSManager {
"auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label),
"hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label),
"sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label),
"magic_mount_feature_label" to context.getString(R.string.magic_mount_feature_label),
"sus_su_feature_label" to context.getString(R.string.sus_su_feature_label)
)
@ -859,31 +917,6 @@ object SuSFSManager {
}.sortedBy { it.name }
}
private fun parseEnabledFeaturesFromStatus(context: Context, status: Natives.SusfsFeatureStatus): List<EnabledFeature> {
val featureList = listOf(
Triple("status_sus_path", context.getString(R.string.sus_path_feature_label), status.statusSusPath),
Triple("status_sus_mount", context.getString(R.string.sus_mount_feature_label), status.statusSusMount),
Triple("status_try_umount", context.getString(R.string.try_umount_feature_label), status.statusTryUmount),
Triple("status_spoof_uname", context.getString(R.string.spoof_uname_feature_label), status.statusSpoofUname),
Triple("status_spoof_cmdline", context.getString(R.string.spoof_cmdline_feature_label), status.statusSpoofCmdline),
Triple("status_open_redirect", context.getString(R.string.open_redirect_feature_label), status.statusOpenRedirect),
Triple("status_enable_log", context.getString(R.string.enable_log_feature_label), status.statusEnableLog),
Triple("status_auto_default_mount", context.getString(R.string.auto_default_mount_feature_label), status.statusAutoDefaultMount),
Triple("status_auto_bind_mount", context.getString(R.string.auto_bind_mount_feature_label), status.statusAutoBindMount),
Triple("status_auto_try_umount_bind", context.getString(R.string.auto_try_umount_bind_feature_label), status.statusAutoTryUmountBind),
Triple("status_hide_symbols", context.getString(R.string.hide_symbols_feature_label), status.statusHideSymbols),
Triple("status_sus_kstat", context.getString(R.string.sus_kstat_feature_label), status.statusSusKstat),
Triple("status_magic_mount", context.getString(R.string.magic_mount_feature_label), status.statusMagicMount),
Triple("status_sus_su", context.getString(R.string.sus_su_feature_label), status.statusSusSu)
)
return featureList.map { (id, displayName, isEnabled) ->
val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled)
val canConfigure = id == "status_enable_log"
EnabledFeature(displayName, isEnabled, statusText, canConfigure)
}.sortedBy { it.name }
}
// sus日志开关
suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean {
val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}")
@ -1107,6 +1140,54 @@ object SuSFSManager {
}
}
// 添加 SUS Maps
suspend fun addSusMap(context: Context, map: String): Boolean {
val success = executeSusfsCommand(context, "add_sus_map '$map'")
if (success) {
saveSusMaps(context, getSusMaps(context) + map)
if (isAutoStartEnabled(context)) updateMagiskModule(context)
showToast(context, context.getString(R.string.susfs_sus_map_added_success, map))
}
return success
}
suspend fun removeSusMap(context: Context, map: String): Boolean {
saveSusMaps(context, getSusMaps(context) - map)
if (isAutoStartEnabled(context)) updateMagiskModule(context)
showToast(context, context.getString(R.string.susfs_sus_map_removed, map))
return true
}
suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean {
return try {
val currentMaps = getSusMaps(context).toMutableSet()
if (!currentMaps.remove(oldMap)) {
showToast(context, "Original SUS map not found: $oldMap")
return false
}
saveSusMaps(context, currentMaps)
val success = addSusMap(context, newMap)
if (success) {
showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap))
return true
} else {
// 如果添加新映射失败,恢复旧映射
currentMaps.add(oldMap)
saveSusMaps(context, currentMaps)
if (isAutoStartEnabled(context)) updateMagiskModule(context)
showToast(context, "Failed to update SUS map, reverted to original")
return false
}
} catch (e: Exception) {
e.printStackTrace()
showToast(context, "Error updating SUS map: ${e.message}")
false
}
}
// 添加SUS挂载
suspend fun addSusMount(context: Context, mount: String): Boolean {
val success = executeSusfsCommand(context, "add_sus_mount '$mount'")
@ -1208,8 +1289,6 @@ object SuSFSManager {
}
}
suspend fun runTryUmount(context: Context): Boolean = executeSusfsCommand(context, "run_try_umount")
// Zygote隔离服务卸载控制
suspend fun setUmountForZygoteIsoService(context: Context, enabled: Boolean): Boolean {
if (!isSusVersion158()) {
@ -1361,7 +1440,7 @@ object SuSFSManager {
if (success) {
saveAndroidDataPath(context, path)
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}
@ -1375,7 +1454,7 @@ object SuSFSManager {
if (success) {
saveSdcardPath(context, path)
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
CoroutineScope(Dispatchers.Default).launch {
updateMagiskModule(context)
}
}

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.susfs.util
import android.annotation.SuppressLint
@ -497,6 +497,10 @@ object ScriptGenerator {
if (config.susLoopPaths.isNotEmpty()) {
generateSusLoopPathsSection(config.susLoopPaths)
}
if (config.susMaps.isNotEmpty()) {
generateSusMapsSection(config.susMaps)
}
}
}
@ -504,6 +508,17 @@ object ScriptGenerator {
}
}
private fun StringBuilder.generateSusMapsSection(susMaps: Set<String>) {
if (susMaps.isNotEmpty()) {
appendLine("# 添加SUS映射")
susMaps.forEach { map ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'")
appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
}
@SuppressLint("SdCardPath")
private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) {
appendLine("# 路径配置")

View file

@ -6,114 +6,187 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Stable
object CardConfig {
// 卡片透明度
var cardAlpha by mutableFloatStateOf(1f)
internal set
// 卡片亮度
var cardDim by mutableFloatStateOf(0f)
internal set
// 卡片阴影
var cardElevation by mutableStateOf(0.dp)
var isShadowEnabled by mutableStateOf(true)
var isCustomAlphaSet by mutableStateOf(false)
var isCustomDimSet by mutableStateOf(false)
var isUserDarkModeEnabled by mutableStateOf(false)
var isUserLightModeEnabled by mutableStateOf(false)
var isCustomBackgroundEnabled by mutableStateOf(false)
internal set
// 功能开关
var isShadowEnabled by mutableStateOf(true)
internal set
var isCustomBackgroundEnabled by mutableStateOf(false)
internal set
var isCustomAlphaSet by mutableStateOf(false)
internal set
var isCustomDimSet by mutableStateOf(false)
internal set
var isUserDarkModeEnabled by mutableStateOf(false)
internal set
var isUserLightModeEnabled by mutableStateOf(false)
internal set
// 配置键名
private object Keys {
const val CARD_ALPHA = "card_alpha"
const val CARD_DIM = "card_dim"
const val CUSTOM_BACKGROUND_ENABLED = "custom_background_enabled"
const val IS_SHADOW_ENABLED = "is_shadow_enabled"
const val IS_CUSTOM_ALPHA_SET = "is_custom_alpha_set"
const val IS_CUSTOM_DIM_SET = "is_custom_dim_set"
const val IS_USER_DARK_MODE_ENABLED = "is_user_dark_mode_enabled"
const val IS_USER_LIGHT_MODE_ENABLED = "is_user_light_mode_enabled"
}
fun updateAlpha(alpha: Float, isCustom: Boolean = true) {
cardAlpha = alpha.coerceIn(0f, 1f)
if (isCustom) isCustomAlphaSet = true
}
fun updateDim(dim: Float, isCustom: Boolean = true) {
cardDim = dim.coerceIn(0f, 1f)
if (isCustom) isCustomDimSet = true
}
fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) {
isShadowEnabled = enabled
cardElevation = if (enabled) elevation else cardElevation
}
fun updateBackground(enabled: Boolean) {
isCustomBackgroundEnabled = enabled
// 自定义背景时自动禁用阴影以获得更好的视觉效果
if (enabled) {
updateShadow(false)
}
}
fun updateThemePreference(darkMode: Boolean?, lightMode: Boolean?) {
isUserDarkModeEnabled = darkMode ?: false
isUserLightModeEnabled = lightMode ?: false
}
fun reset() {
cardAlpha = 1f
cardDim = 0f
cardElevation = 0.dp
isShadowEnabled = true
isCustomBackgroundEnabled = false
isCustomAlphaSet = false
isCustomDimSet = false
isUserDarkModeEnabled = false
isUserLightModeEnabled = false
}
fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) {
updateAlpha(if (isDarkMode) 0.88f else 1f, false)
}
if (!isCustomDimSet) {
updateDim(if (isDarkMode) 0.25f else 0f, false)
}
// 暗色模式下默认启用轻微阴影
if (isDarkMode && !isCustomBackgroundEnabled) {
updateShadow(true, 2.dp)
}
}
/**
* 保存卡片配置到SharedPreferences
*/
fun save(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE)
prefs.edit().apply {
putFloat("card_alpha", cardAlpha)
putFloat("card_dim", cardDim)
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
putBoolean("is_shadow_enabled", isShadowEnabled)
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
putBoolean("is_custom_dim_set", isCustomDimSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
putFloat(Keys.CARD_ALPHA, cardAlpha)
putFloat(Keys.CARD_DIM, cardDim)
putBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, isCustomBackgroundEnabled)
putBoolean(Keys.IS_SHADOW_ENABLED, isShadowEnabled)
putBoolean(Keys.IS_CUSTOM_ALPHA_SET, isCustomAlphaSet)
putBoolean(Keys.IS_CUSTOM_DIM_SET, isCustomDimSet)
putBoolean(Keys.IS_USER_DARK_MODE_ENABLED, isUserDarkModeEnabled)
putBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, isUserLightModeEnabled)
apply()
}
}
/**
* 从SharedPreferences加载卡片配置
*/
fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 1f)
cardDim = prefs.getFloat("card_dim", 0f)
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
updateShadowEnabled(isShadowEnabled)
val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat(Keys.CARD_ALPHA, 1f).coerceIn(0f, 1f)
cardDim = prefs.getFloat(Keys.CARD_DIM, 0f).coerceIn(0f, 1f)
isCustomBackgroundEnabled = prefs.getBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, false)
isShadowEnabled = prefs.getBoolean(Keys.IS_SHADOW_ENABLED, true)
isCustomAlphaSet = prefs.getBoolean(Keys.IS_CUSTOM_ALPHA_SET, false)
isCustomDimSet = prefs.getBoolean(Keys.IS_CUSTOM_DIM_SET, false)
isUserDarkModeEnabled = prefs.getBoolean(Keys.IS_USER_DARK_MODE_ENABLED, false)
isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false)
// 应用阴影设置
updateShadow(isShadowEnabled, if (isShadowEnabled) cardElevation else 0.dp)
}
/**
* 更新阴影启用状态
*/
@Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)"))
fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled
cardElevation = 0.dp
}
/**
* 设置主题模式默认值
*/
fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) {
cardAlpha = 1f
}
if (!isCustomDimSet) {
cardDim = if (isDarkMode) 0.5f else 0f
}
updateShadowEnabled(isShadowEnabled)
updateShadow(enabled)
}
}
/**
* 获取卡片颜色配置
*/
@Composable
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = determineContentColor(originalColor)
)
object CardStyleProvider {
/**
* 获取卡片阴影配置
*/
@Composable
fun getCardElevation() = CardDefaults.cardElevation(
defaultElevation = CardConfig.cardElevation,
pressedElevation = CardConfig.cardElevation,
focusedElevation = CardConfig.cardElevation,
hoveredElevation = CardConfig.cardElevation,
draggedElevation = CardConfig.cardElevation,
disabledElevation = CardConfig.cardElevation
)
@Composable
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = determineContentColor(originalColor),
disabledContainerColor = originalColor.copy(alpha = CardConfig.cardAlpha * 0.38f),
disabledContentColor = determineContentColor(originalColor).copy(alpha = 0.38f)
)
/**
* 根据背景颜色主题模式和用户设置确定内容颜色
*/
@Composable
private fun determineContentColor(originalColor: Color): Color {
val isDarkTheme = isSystemInDarkTheme()
if (ThemeConfig.isThemeChanging) {
return if (isDarkTheme) Color.White else Color.Black
}
@Composable
fun getCardElevation() = CardDefaults.cardElevation(
defaultElevation = CardConfig.cardElevation,
pressedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 0).dp
} else 0.dp,
focusedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 0).dp
} else 0.dp,
hoveredElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 0).dp
} else 0.dp,
draggedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 0).dp
} else 0.dp,
disabledElevation = 0.dp
)
return when {
CardConfig.isUserLightModeEnabled -> Color.Black
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
isDarkTheme -> Color.White
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
@Composable
private fun determineContentColor(originalColor: Color): Color {
val isDarkTheme = isSystemInDarkTheme()
return when {
ThemeConfig.isThemeChanging -> {
if (isDarkTheme) Color.White else Color.Black
}
CardConfig.isUserLightModeEnabled -> Color.Black
CardConfig.isUserDarkModeEnabled -> Color.White
else -> {
val luminance = originalColor.luminance()
val threshold = if (isDarkTheme) 0.4f else 0.6f
if (luminance > threshold) Color.Black else Color.White
}
}
}
}
// 向后兼容
@Composable
fun getCardColors(originalColor: Color) = CardStyleProvider.getCardColors(originalColor)
@Composable
fun getCardElevation() = CardStyleProvider.getCardElevation()

View file

@ -1,6 +1,5 @@
package com.sukisu.ultra.ui.theme
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Build
@ -9,9 +8,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
@ -28,36 +25,38 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.content.edit
import androidx.core.net.toUri
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
/**
* 主题配置对象管理应用的主题相关状态
*/
@Stable
object ThemeConfig {
// 主题状态
var customBackgroundUri by mutableStateOf<Uri?>(null)
var forceDarkMode by mutableStateOf<Boolean?>(null)
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
var useDynamicColor by mutableStateOf(false)
// 背景状态
var backgroundImageLoaded by mutableStateOf(false)
var needsResetOnThemeChange by mutableStateOf(false)
var isThemeChanging by mutableStateOf(false)
var preventBackgroundRefresh by mutableStateOf(false)
// 主题变化检测
private var lastDarkModeState: Boolean? = null
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
val hasChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
lastDarkModeState = currentDarkMode
return isChanged
return hasChanged
}
fun resetBackgroundState() {
@ -66,11 +65,171 @@ object ThemeConfig {
}
isThemeChanging = true
}
fun updateTheme(
theme: ThemeColors? = null,
dynamicColor: Boolean? = null,
darkMode: Boolean? = null
) {
theme?.let { currentTheme = it }
dynamicColor?.let { useDynamicColor = it }
darkMode?.let { forceDarkMode = it }
}
fun reset() {
customBackgroundUri = null
forceDarkMode = null
currentTheme = ThemeColors.Default
useDynamicColor = false
backgroundImageLoaded = false
isThemeChanging = false
preventBackgroundRefresh = false
lastDarkModeState = null
}
}
object ThemeManager {
private const val PREFS_NAME = "theme_prefs"
fun saveThemeMode(context: Context, forceDark: Boolean?) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putString("theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
})
}
ThemeConfig.forceDarkMode = forceDark
}
fun loadThemeMode(context: Context) {
val mode = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when (mode) {
"dark" -> true
"light" -> false
else -> null
}
}
fun saveThemeColors(context: Context, themeName: String) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
}
fun loadThemeColors(context: Context) {
val themeName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString("theme_colors", "default") ?: "default"
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
}
fun saveDynamicColorState(context: Context, enabled: Boolean) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
fun loadDynamicColorState(context: Context) {
val enabled = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
ThemeConfig.useDynamicColor = enabled
}
}
object BackgroundManager {
private const val TAG = "BackgroundManager"
fun saveAndApplyCustomBackground(
context: Context,
uri: Uri,
transformation: BackgroundTransformation? = null
) {
try {
val finalUri = if (transformation != null) {
context.saveTransformedBackground(uri, transformation)
} else {
copyImageToInternalStorage(context, uri)
}
saveBackgroundUri(context, finalUri)
ThemeConfig.customBackgroundUri = finalUri
CardConfig.updateBackground(true)
resetBackgroundState(context)
} catch (e: Exception) {
Log.e(TAG, "保存背景失败: ${e.message}", e)
}
}
fun clearCustomBackground(context: Context) {
saveBackgroundUri(context, null)
ThemeConfig.customBackgroundUri = null
CardConfig.updateBackground(false)
resetBackgroundState(context)
}
fun loadCustomBackground(context: Context) {
val uriString = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
val newUri = uriString?.toUri()
val preventRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d(TAG, "加载自定义背景: $uriString")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
CardConfig.updateBackground(newUri != null)
}
}
private fun saveBackgroundUri(context: Context, uri: Uri?) {
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putString("custom_background", uri?.toString())
putBoolean("prevent_background_refresh", false)
}
}
private fun resetBackgroundState(context: Context) {
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
}
private fun copyImageToInternalStorage(context: Context, uri: Uri): Uri? {
return try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background_${System.currentTimeMillis()}.jpg"
val file = File(context.filesDir, fileName)
FileOutputStream(file).use { outputStream ->
val buffer = ByteArray(8 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
outputStream.flush()
}
inputStream.close()
Uri.fromFile(file)
} catch (e: Exception) {
Log.e(TAG, "复制图片失败: ${e.message}", e)
null
}
}
}
/**
* 应用主题
*/
@Composable
fun KernelSUTheme(
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
@ -84,198 +243,223 @@ fun KernelSUTheme(
val context = LocalContext.current
val systemIsDark = isSystemInDarkTheme()
// 检测系统主题变化并保存状态
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
LaunchedEffect(systemIsDark, themeChanged) {
if (ThemeConfig.forceDarkMode == null && themeChanged) {
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
ThemeConfig.resetBackgroundState()
if (!ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
}
CardConfig.apply {
load(context)
if (!isCustomAlphaSet) {
cardAlpha = if (systemIsDark) 0.50f else 1f
}
if (!isCustomDimSet) {
cardDim = if (systemIsDark) 0.5f else 0f
}
save(context)
}
}
}
SystemBarStyle(
darkMode = darkTheme
)
// 初始加载配置
LaunchedEffect(Unit) {
context.loadThemeMode()
context.loadThemeColors()
context.loadDynamicColorState()
CardConfig.load(context)
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
ThemeConfig.backgroundImageLoaded = false
}
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", true)
}
// 初始化主题
ThemeInitializer(context = context, systemIsDark = systemIsDark)
// 创建颜色方案
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
val colorScheme = createColorScheme(context, darkTheme, dynamicColor)
// 根据暗色模式和自定义背景调整卡片配置
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
if (darkTheme && !dynamicColor) {
CardConfig.setThemeDefaults(true)
} else if (!darkTheme && !dynamicColor) {
CardConfig.setThemeDefaults(false)
}
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
LaunchedEffect(ThemeConfig.customBackgroundUri) {
backgroundUri.value = ThemeConfig.customBackgroundUri
}
val bgImagePainter = backgroundUri.value?.let {
rememberAsyncImagePainter(
model = it,
onError = { err ->
Log.e("ThemeSystem", "背景图加载失败: ${err.result.throwable.message}")
ThemeConfig.customBackgroundUri = null
context.saveCustomBackground(null)
},
onSuccess = {
Log.d("ThemeSystem", "背景图加载成功")
ThemeConfig.backgroundImageLoaded = true
ThemeConfig.isThemeChanging = false
ThemeConfig.preventBackgroundRefresh = true
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { putBoolean("prevent_background_refresh", true) }
}
)
}
val transition = updateTransition(
targetState = ThemeConfig.backgroundImageLoaded,
label = "bgTransition"
)
val bgAlpha by transition.animateFloat(
label = "bgAlpha",
transitionSpec = {
spring(
dampingRatio = 0.8f,
stiffness = 300f
)
}
) { loaded -> if (loaded) 1f else 0f }
DisposableEffect(systemIsDark) {
onDispose {
if (ThemeConfig.isThemeChanging) {
ThemeConfig.isThemeChanging = false
}
}
}
// 计算适用的暗化值
val dimFactor = CardConfig.cardDim
// 系统栏样式
SystemBarController(darkTheme)
MaterialTheme(
colorScheme = colorScheme,
typography = Typography
) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-2f)
.background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
)
// 自定义背景层
backgroundUri.value?.let {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
.alpha(bgAlpha)
) {
// 背景图片
bgImagePainter?.let { painter ->
Box(
modifier = Modifier
.fillMaxSize()
.paint(
painter = painter,
contentScale = ContentScale.Crop
)
.graphicsLayer {
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
}
)
}
// 亮度调节层 (根据cardDim调整)
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
)
)
// 边缘渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
),
radius = 1200f
)
)
)
}
}
// 背景层
BackgroundLayer(darkTheme)
// 内容层
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f)
) {
Box(modifier = Modifier.fillMaxSize().zIndex(1f)) {
content()
}
}
}
}
/**
* 创建动态深色颜色方案
*/
@Composable
private fun ThemeInitializer(context: Context, systemIsDark: Boolean) {
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
val scope = rememberCoroutineScope()
// 处理系统主题变化
LaunchedEffect(systemIsDark, themeChanged) {
if (ThemeConfig.forceDarkMode == null && themeChanged) {
Log.d("ThemeSystem", "系统主题变化: $systemIsDark")
ThemeConfig.resetBackgroundState()
if (!ThemeConfig.preventBackgroundRefresh) {
BackgroundManager.loadCustomBackground(context)
}
CardConfig.apply {
load(context)
setThemeDefaults(systemIsDark)
save(context)
}
}
}
// 初始加载配置
LaunchedEffect(Unit) {
scope.launch {
ThemeManager.loadThemeMode(context)
ThemeManager.loadThemeColors(context)
ThemeManager.loadDynamicColorState(context)
CardConfig.load(context)
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
BackgroundManager.loadCustomBackground(context)
}
}
}
}
@Composable
private fun BackgroundLayer(darkTheme: Boolean) {
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
LaunchedEffect(ThemeConfig.customBackgroundUri) {
backgroundUri.value = ThemeConfig.customBackgroundUri
}
// 默认背景
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-2f)
.background(
if (CardConfig.isCustomBackgroundEnabled) {
MaterialTheme.colorScheme.surfaceContainerLow
} else {
MaterialTheme.colorScheme.background
}
)
)
// 自定义背景
backgroundUri.value?.let { uri ->
CustomBackgroundLayer(uri = uri, darkTheme = darkTheme)
}
}
@Composable
private fun CustomBackgroundLayer(uri: Uri, darkTheme: Boolean) {
val painter = rememberAsyncImagePainter(
model = uri,
onError = { error ->
Log.e("ThemeSystem", "背景加载失败: ${error.result.throwable.message}")
ThemeConfig.customBackgroundUri = null
},
onSuccess = {
Log.d("ThemeSystem", "背景加载成功")
ThemeConfig.backgroundImageLoaded = true
ThemeConfig.isThemeChanging = false
}
)
val transition = updateTransition(
targetState = ThemeConfig.backgroundImageLoaded,
label = "backgroundTransition"
)
val alpha by transition.animateFloat(
label = "backgroundAlpha",
transitionSpec = {
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
}
) { loaded -> if (loaded) 1f else 0f }
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
.alpha(alpha)
) {
// 背景图片
Box(
modifier = Modifier
.fillMaxSize()
.paint(painter = painter, contentScale = ContentScale.Crop)
.graphicsLayer {
this.alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
}
)
// 遮罩层
BackgroundOverlay(darkTheme = darkTheme)
}
}
@Composable
private fun BackgroundOverlay(darkTheme: Boolean) {
val dimFactor = CardConfig.cardDim
// 主要遮罩层
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) {
Color.Black.copy(alpha = 0.3f + dimFactor * 0.4f)
} else {
Color.White.copy(alpha = 0.05f + dimFactor * 0.3f)
}
)
)
// 边缘渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) {
Color.Black.copy(alpha = 0.2f + dimFactor * 0.2f)
} else {
Color.Black.copy(alpha = 0.05f + dimFactor * 0.1f)
}
),
radius = 1000f
)
)
)
}
@Composable
private fun createColorScheme(
context: Context,
darkTheme: Boolean,
dynamicColor: Boolean
): ColorScheme {
return when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context)
else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
}
@Composable
private fun SystemBarController(darkMode: Boolean) {
val context = LocalContext.current
val activity = context as ComponentActivity
SideEffect {
activity.enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
Color.Transparent.toArgb(),
Color.Transparent.toArgb(),
) { darkMode },
navigationBarStyle = if (darkMode) {
SystemBarStyle.dark(Color.Transparent.toArgb())
} else {
SystemBarStyle.light(
Color.Transparent.toArgb(),
Color.Transparent.toArgb()
)
}
)
}
}
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
@ -288,9 +472,6 @@ private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
)
}
/**
* 创建动态浅色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicLightColorScheme(context: Context): ColorScheme {
@ -303,11 +484,6 @@ private fun createDynamicLightColorScheme(context: Context): ColorScheme {
)
}
/**
* 创建深色颜色方案
*/
@Composable
private fun createDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.primaryDark,
@ -347,9 +523,6 @@ private fun createDarkColorScheme() = darkColorScheme(
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark,
)
/**
* 创建浅色颜色方案
*/
@Composable
private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.primaryLight,
@ -389,218 +562,32 @@ private fun createLightColorScheme() = lightColorScheme(
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
)
/**
* 复制图片到应用内部存储并提升持久性
*/
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
return try {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background.jpg"
val file = File(filesDir, fileName)
val backupFile = File(filesDir, "${fileName}.backup")
val outputStream = FileOutputStream(backupFile)
val buffer = ByteArray(4 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
outputStream.flush()
outputStream.close()
inputStream.close()
if (file.exists()) {
file.delete()
}
backupFile.renameTo(file)
Uri.fromFile(file)
} catch (e: Exception) {
Log.e("ImageCopy", "复制图片失败: ${e.message}")
null
}
}
/**
* 保存并应用自定义背景
*/
// 向后兼容
@OptIn(DelicateCoroutinesApi::class)
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
val finalUri = if (transformation != null) {
saveTransformedBackground(uri, transformation)
} else {
copyImageToInternalStorage(uri)
kotlinx.coroutines.GlobalScope.launch {
BackgroundManager.saveAndApplyCustomBackground(this@saveAndApplyCustomBackground, uri, transformation)
}
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", finalUri?.toString())
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
ThemeConfig.customBackgroundUri = finalUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
}
/**
* 保存自定义背景
*/
fun Context.saveCustomBackground(uri: Uri?) {
val newUri = uri?.let { copyImageToInternalStorage(it) }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", newUri?.toString())
if (uri == null) {
// 如果清除背景,也重置阻止刷新标志
putBoolean("prevent_background_refresh", false)
} else {
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
}
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
if (uri != null) {
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
saveAndApplyCustomBackground(uri)
} else {
BackgroundManager.clearCustomBackground(this)
}
}
/**
* 加载自定义背景
*/
fun Context.loadCustomBackground() {
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
val newUri = uriString?.toUri()
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
}
}
/**
* 保存主题模式
*/
fun Context.saveThemeMode(forceDark: Boolean?) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString(
"theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
}
)
}
ThemeConfig.forceDarkMode = forceDark
ThemeConfig.needsResetOnThemeChange = forceDark == null
ThemeManager.saveThemeMode(this, forceDark)
}
/**
* 加载主题模式
*/
fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) {
"dark" -> true
"light" -> false
else -> null
}
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
}
/**
* 保存主题颜色
*/
fun Context.saveThemeColors(themeName: String) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
ThemeManager.saveThemeColors(this, themeName)
}
/**
* 加载主题颜色
*/
fun Context.loadThemeColors() {
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_colors", "default")
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
}
/**
* 保存动态颜色状态
*/
fun Context.saveDynamicColorState(enabled: Boolean) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
/**
* 加载动态颜色状态
*/
fun Context.loadDynamicColorState() {
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled
}
@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(),
)
}
)
}
ThemeManager.saveDynamicColorState(this, enabled)
}

View file

@ -0,0 +1,411 @@
package com.sukisu.ultra.ui.theme.component
import android.net.Uri
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.max
@Composable
fun ImageEditorDialog(
imageUri: Uri,
onDismiss: () -> Unit,
onConfirm: (Uri) -> Unit
) {
// 图像变换状态
val transformState = remember { ImageTransformState() }
val context = LocalContext.current
val scope = rememberCoroutineScope()
// 尺寸状态
var imageSize by remember { mutableStateOf(Size.Zero) }
var screenSize by remember { mutableStateOf(Size.Zero) }
// 动画状态
val animationSpec = spring<Float>(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
val animatedScale by animateFloatAsState(
targetValue = transformState.scale,
animationSpec = animationSpec,
label = "ScaleAnimation"
)
val animatedOffsetX by animateFloatAsState(
targetValue = transformState.offsetX,
animationSpec = animationSpec,
label = "OffsetXAnimation"
)
val animatedOffsetY by animateFloatAsState(
targetValue = transformState.offsetY,
animationSpec = animationSpec,
label = "OffsetYAnimation"
)
// 工具函数
val scaleToFullScreen = remember {
{
if (imageSize.height > 0 && screenSize.height > 0) {
val newScale = screenSize.height / imageSize.height
transformState.updateTransform(newScale, 0f, 0f)
}
}
}
val saveImage: () -> Unit = remember {
{
scope.launch {
try {
val transformation = BackgroundTransformation(
transformState.scale,
transformState.offsetX,
transformState.offsetY
)
val savedUri = context.saveTransformedBackground(imageUri, transformation)
savedUri?.let { onConfirm(it) }
} catch (_: Exception) {
}
}
}
}
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Black.copy(alpha = 0.9f),
Color.Black.copy(alpha = 0.95f)
),
radius = 800f
)
)
.onSizeChanged { size ->
screenSize = Size(size.width.toFloat(), size.height.toFloat())
}
) {
// 图像显示区域
ImageDisplayArea(
imageUri = imageUri,
animatedScale = animatedScale,
animatedOffsetX = animatedOffsetX,
animatedOffsetY = animatedOffsetY,
transformState = transformState,
onImageSizeChanged = { imageSize = it },
modifier = Modifier.fillMaxSize()
)
// 顶部工具栏
TopToolbar(
onDismiss = onDismiss,
onFullscreen = scaleToFullScreen,
onConfirm = saveImage,
modifier = Modifier.align(Alignment.TopCenter)
)
// 底部提示信息
BottomHintCard(
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
/**
* 图像变换状态管理类
*/
private class ImageTransformState {
var scale by mutableFloatStateOf(1f)
var offsetX by mutableFloatStateOf(0f)
var offsetY by mutableFloatStateOf(0f)
private var lastScale = 1f
private var lastOffsetX = 0f
private var lastOffsetY = 0f
fun updateTransform(newScale: Float, newOffsetX: Float, newOffsetY: Float) {
val scaleDiff = abs(newScale - lastScale)
val offsetXDiff = abs(newOffsetX - lastOffsetX)
val offsetYDiff = abs(newOffsetY - lastOffsetY)
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
scale = newScale
offsetX = newOffsetX
offsetY = newOffsetY
lastScale = newScale
lastOffsetX = newOffsetX
lastOffsetY = newOffsetY
}
}
fun resetToLast() {
scale = lastScale
offsetX = lastOffsetX
offsetY = lastOffsetY
}
}
/**
* 图像显示区域组件
*/
@Composable
private fun ImageDisplayArea(
imageUri: Uri,
animatedScale: Float,
animatedOffsetX: Float,
animatedOffsetY: Float,
transformState: ImageTransformState,
onImageSizeChanged: (Size) -> Unit,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUri)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.settings_custom_background),
contentScale = ContentScale.Fit,
modifier = modifier
.graphicsLayer(
scaleX = animatedScale,
scaleY = animatedScale,
translationX = animatedOffsetX,
translationY = animatedOffsetY
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scope.launch {
try {
val newScale = (transformState.scale * zoom).coerceIn(0.5f, 3f)
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
val newOffsetX = if (maxOffsetX > 0) {
(transformState.offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
} else 0f
val newOffsetY = if (maxOffsetY > 0) {
(transformState.offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
} else 0f
transformState.updateTransform(newScale, newOffsetX, newOffsetY)
} catch (_: Exception) {
transformState.resetToLast()
}
}
}
}
.onSizeChanged { size ->
onImageSizeChanged(Size(size.width.toFloat(), size.height.toFloat()))
}
)
}
/**
* 顶部工具栏组件
*/
@Composable
private fun TopToolbar(
onDismiss: () -> Unit,
onFullscreen: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
// 关闭按钮
ActionButton(
onClick = onDismiss,
icon = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
backgroundColor = MaterialTheme.colorScheme.error.copy(alpha = 0.9f)
)
// 全屏按钮
ActionButton(
onClick = onFullscreen,
icon = Icons.Default.Fullscreen,
contentDescription = stringResource(R.string.reprovision),
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
)
// 确认按钮
ActionButton(
onClick = onConfirm,
icon = Icons.Default.Check,
contentDescription = stringResource(R.string.confirm),
backgroundColor = Color(0xFF4CAF50).copy(alpha = 0.9f)
)
}
}
/**
* 操作按钮组件
*/
@Composable
private fun ActionButton(
onClick: () -> Unit,
icon: androidx.compose.ui.graphics.vector.ImageVector,
contentDescription: String,
backgroundColor: Color,
modifier: Modifier = Modifier
) {
var isPressed by remember { mutableStateOf(false) }
val buttonScale by animateFloatAsState(
targetValue = if (isPressed) 0.85f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "ButtonScale"
)
val buttonAlpha by animateFloatAsState(
targetValue = if (isPressed) 0.8f else 1f,
animationSpec = tween(100),
label = "ButtonAlpha"
)
Surface(
onClick = {
isPressed = true
onClick()
},
modifier = modifier
.size(64.dp)
.graphicsLayer(
scaleX = buttonScale,
scaleY = buttonScale,
alpha = buttonAlpha
),
shape = CircleShape,
color = backgroundColor,
shadowElevation = 8.dp
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
}
LaunchedEffect(isPressed) {
if (isPressed) {
kotlinx.coroutines.delay(150)
isPressed = false
}
}
}
/**
* 底部提示卡片组件
*/
@Composable
private fun BottomHintCard(
modifier: Modifier = Modifier
) {
var isVisible by remember { mutableStateOf(true) }
val cardAlpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(
durationMillis = 500,
easing = EaseInOutCubic
),
label = "HintAlpha"
)
val cardTranslationY by animateFloatAsState(
targetValue = if (isVisible) 0f else 100f,
animationSpec = tween(
durationMillis = 500,
easing = EaseInOutCubic
),
label = "HintTranslation"
)
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(4000)
isVisible = false
}
Card(
modifier = modifier
.fillMaxWidth()
.padding(24.dp)
.alpha(cardAlpha)
.graphicsLayer {
translationY = cardTranslationY
},
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.85f)
),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Text(
text = stringResource(id = R.string.image_editor_hint),
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
)
}
}

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.theme.util
import android.content.ContentResolver
import android.content.Context

View file

@ -13,6 +13,7 @@ import android.util.Log
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ -21,6 +22,7 @@ import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import org.json.JSONArray
import java.io.File
import java.util.concurrent.CountDownLatch
/**
@ -30,11 +32,11 @@ import java.io.File
private const val TAG = "KsuCli"
private fun getKsuDaemonPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so"
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
}
object KsuCli {
val SHELL: Shell = createRootShell()
var SHELL: Shell = createRootShell()
val GLOBAL_MNT_SHELL: Shell = createRootShell(true)
}
@ -99,7 +101,7 @@ fun execKsud(args: String, newShell: Boolean = false): Boolean {
fun install() {
val start = SystemClock.elapsedRealtime()
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so").absolutePath
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so").absolutePath
val result = execKsud("install --magiskboot $magiskboot", true)
Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms")
}
@ -222,7 +224,7 @@ fun runModuleAction(
fun restoreBoot(
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
val result = flashWithIO(
"${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot",
onStdout,
@ -235,7 +237,7 @@ fun restoreBoot(
fun uninstallPermanently(
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
val result =
flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr)
onFinish(result.isSuccess, result.code)
@ -253,6 +255,7 @@ fun installBoot(
bootUri: Uri?,
lkm: LkmSelection,
ota: Boolean,
partition: String?,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit,
@ -270,7 +273,7 @@ fun installBoot(
}
}
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}"
cmd += if (bootFile == null) {
@ -312,6 +315,10 @@ fun installBoot(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
cmd += " -o $downloadsDir"
partition?.let { part ->
cmd += " --partition $part"
}
val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
Log.i("KernelSU", "install boot result: ${result.isSuccess}")
@ -320,6 +327,11 @@ fun installBoot(
// if boot uri is empty, it is direct install, when success, we should show reboot button
onFinish(bootUri == null && result.isSuccess, result.code)
if (bootUri == null && result.isSuccess) {
install()
}
return result.isSuccess
}
@ -337,14 +349,6 @@ fun rootAvailable(): Boolean {
return shell.isRoot
}
fun isAbDevice(): Boolean {
val shell = getRootShell()
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
}
fun isInitBoot(): Boolean {
return !Os.uname().release.contains("android12-")
}
suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
@ -354,7 +358,40 @@ suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info supported-kmi"
val cmd = "boot-info supported-kmis"
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() }
}
suspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info is-ab-device"
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim().toBoolean()
}
suspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
if (shell.isRoot) {
val cmd = "boot-info default-partition"
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim()
} else {
if (!Os.uname().release.contains("android12-")) "init_boot" else "boot"
}
}
suspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = if (ota) {
"boot-info slot-suffix --ota"
} else {
"boot-info slot-suffix"
}
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim()
}
suspend fun getAvailablePartitions(): List<String> = withContext(Dispatchers.IO) {
val shell = getRootShell()
val cmd = "boot-info available-partitions"
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
out.filter { it.isNotBlank() }.map { it.trim() }
}
@ -419,6 +456,69 @@ fun deleteAppProfileTemplate(id: String): Boolean {
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
.to(ArrayList(), null).exec().isSuccess
}
// KPM控制
fun loadKpmModule(path: String, args: String? = null): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm load $path ${args ?: ""}"
return ShellUtils.fastCmd(shell, cmd)
}
fun unloadKpmModule(name: String): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm unload $name"
return ShellUtils.fastCmd(shell, cmd)
}
fun getKpmModuleCount(): Int {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm num"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim().toIntOrNull() ?: 0
}
fun runCmd(shell: Shell, cmd: String): String {
return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null)
.exec().out
.joinToString("\n")
}
fun listKpmModules(): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm list"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to list KPM modules", e)
""
}
}
fun getKpmModuleInfo(name: String): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm info $name"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to get KPM module info: $name", e)
""
}
}
fun controlKpmModule(name: String, args: String? = null): Int {
val shell = getRootShell()
val cmd = """${getKsuDaemonPath()} kpm control $name "${args ?: ""}""""
val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1
}
fun getKpmVersion(): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} kpm version"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim()
}
fun forceStopApp(packageName: String) {
val shell = getRootShell()
@ -442,143 +542,59 @@ fun restartApp(packageName: String) {
}
fun getSuSFSDaemonPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
}
fun getSuSFS(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} support")
return result
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksu_susfs.so"
}
fun getSuSFSVersion(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} version")
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show version")
return result
}
fun getSuSFSVariant(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant")
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show variant")
return result
}
fun getSuSFSFeatures(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features")
return result
}
fun susfsSUS_SU_0(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 0")
return result
}
fun susfsSUS_SU_2(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 2")
return result
}
fun susfsSUS_SU_Mode(): String {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode")
return result
}
fun getKpmmgrPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
}
fun loadKpmModule(path: String, args: String? = null): String {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
return ShellUtils.fastCmd(shell, cmd)
}
fun unloadKpmModule(name: String): String {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} unload $name"
return ShellUtils.fastCmd(shell, cmd)
}
fun getKpmModuleCount(): Int {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} num"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim().toIntOrNull() ?: 0
}
fun runCmd(shell: Shell, cmd: String): String {
return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null)
.exec().out
.joinToString("\n")
}
fun listKpmModules(): String {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} list"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to list KPM modules", e)
""
}
}
fun getKpmModuleInfo(name: String): String {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} info $name"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to get KPM module info: $name", e)
""
}
}
fun controlKpmModule(name: String, args: String? = null): Int {
val shell = getRootShell()
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1
}
fun getKpmVersion(): String {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} version"
val result = ShellUtils.fastCmd(shell, cmd)
return result.trim()
val cmd = "${getSuSFSDaemonPath()} show enabled_features"
return runCmd(shell, cmd)
}
fun getZygiskImplement(): String {
val shell = getRootShell()
val zygiskPath = "/data/adb/modules/zygisksu"
val rezygiskPath = "/data/adb/modules/rezygisk"
val result = if (ShellUtils.fastCmdResult(shell, "test -f $zygiskPath/module.prop && test ! -f $zygiskPath/disable")) {
ShellUtils.fastCmd(shell, "grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2")
} else if (ShellUtils.fastCmdResult(shell, "test -f $rezygiskPath/module.prop && test ! -f $rezygiskPath/disable")) {
ShellUtils.fastCmd(shell, "grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2")
} else {
"None"
val zygiskModuleIds = listOf(
"zygisksu",
"rezygisk",
"shirokozygisk"
)
for (moduleId in zygiskModuleIds) {
val modulePath = "/data/adb/modules/$moduleId"
when {
ShellUtils.fastCmdResult(shell, "test -f $modulePath/module.prop && test ! -f $modulePath/disable") -> {
val result = ShellUtils.fastCmd(shell, "grep '^name=' $modulePath/module.prop | cut -d'=' -f2")
Log.i(TAG, "Zygisk implement: $result")
return result
}
}
}
Log.i(TAG, "Zygisk implement: $result")
return result
Log.i(TAG, "Zygisk implement: None")
return "None"
}
fun getUidScannerDaemonPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libuid_scanner.so"
}
private const val targetPath = "/data/adb/uid_scanner"
fun ensureUidScannerExecutable(): Boolean {
val shell = getRootShell()
val uidScannerPath = getUidScannerDaemonPath()
val targetPath = "/data/adb/uid_scanner"
if (!ShellUtils.fastCmdResult(shell, "test -f $targetPath")) {
val copyResult = ShellUtils.fastCmdResult(shell, "cp $uidScannerPath $targetPath")
if (!copyResult) {
@ -589,7 +605,6 @@ fun ensureUidScannerExecutable(): Boolean {
val result = ShellUtils.fastCmdResult(shell, "chmod 755 $targetPath")
return result
}
private const val targetPath = "/data/adb/uid_scanner"
fun setUidAutoScan(enabled: Boolean): Boolean {
val shell = getRootShell()
@ -600,7 +615,10 @@ fun setUidAutoScan(enabled: Boolean): Boolean {
val enableValue = if (enabled) 1 else 0
val cmd = "$targetPath --auto-scan $enableValue && $targetPath reload"
val result = ShellUtils.fastCmdResult(shell, cmd)
return result
val throneResult = Natives.setUidScannerEnabled(enabled)
return result && throneResult
}
fun setUidMultiUserScan(enabled: Boolean): Boolean {
@ -614,3 +632,96 @@ fun setUidMultiUserScan(enabled: Boolean): Boolean {
val result = ShellUtils.fastCmdResult(shell, cmd)
return result
}
fun getUidMultiUserScan(): Boolean {
val shell = getRootShell()
val cmd = "grep 'multi_user_scan=' /data/misc/user_uid/uid_scanner.conf | cut -d'=' -f2"
val result = ShellUtils.fastCmd(shell, cmd).trim()
return try {
result.toInt() == 1
} catch (_: NumberFormatException) {
false
}
}
fun cleanRuntimeEnvironment(): Boolean {
val shell = getRootShell()
return try {
try {
ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop")
} catch (_: Exception) {
}
ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid")
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh")
Natives.clearUidScannerEnvironment()
true
} catch (_: Exception) {
false
}
}
fun readUidScannerFile(): Boolean {
val shell = getRootShell()
return try {
ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1"
} catch (_: Exception) {
false
}
}
fun addUmountPath(path: String, checkMnt: Boolean, flags: Int): Boolean {
val shell = getRootShell()
val checkMntFlag = if (checkMnt) "--check-mnt" else ""
val flagsArg = if (flags >= 0) "--flags $flags" else ""
val cmd = "${getKsuDaemonPath()} umount add $path $checkMntFlag $flagsArg"
val result = ShellUtils.fastCmdResult(shell, cmd)
Log.i(TAG, "add umount path $path result: $result")
return result
}
fun removeUmountPath(path: String): Boolean {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} umount remove $path"
val result = ShellUtils.fastCmdResult(shell, cmd)
Log.i(TAG, "remove umount path $path result: $result")
return result
}
fun listUmountPaths(): String {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} umount list"
return try {
runCmd(shell, cmd).trim()
} catch (e: Exception) {
Log.e(TAG, "Failed to list umount paths", e)
""
}
}
fun clearCustomUmountPaths(): Boolean {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} umount clear-custom"
val result = ShellUtils.fastCmdResult(shell, cmd)
Log.i(TAG, "clear custom umount paths result: $result")
return result
}
fun saveUmountConfig(): Boolean {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} umount save"
val result = ShellUtils.fastCmdResult(shell, cmd)
Log.i(TAG, "save umount config result: $result")
return result
}
fun applyUmountConfigToKernel(): Boolean {
val shell = getRootShell()
val cmd = "${getKsuDaemonPath()} umount apply"
val result = ShellUtils.fastCmdResult(shell, cmd)
Log.i(TAG, "apply umount config to kernel result: $result")
return result
}

View file

@ -1,44 +0,0 @@
package com.sukisu.ultra.ui.util
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import com.sukisu.ultra.ui.MainActivity
/**
* 重启应用程序
**/
fun Context.restartApp(
activityClass: Class<out Activity>,
finishCurrent: Boolean = true,
clearTask: Boolean = true,
newTask: Boolean = true
) {
val intent = Intent(this, activityClass)
if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
if (finishCurrent && this is Activity) {
finish()
}
}
/**
* 刷新启动器图标
*/
fun toggleLauncherIcon(context: Context, useAlt: Boolean) {
val pm = context.packageManager
val main = ComponentName(context, MainActivity::class.java.name)
val alt = ComponentName(context, "${MainActivity::class.java.name}Alias")
if (useAlt) {
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
} else {
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
}
}

View file

@ -1,16 +1,20 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.util.module
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.reboot
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -159,7 +163,8 @@ object ModuleModify {
val moduleDir = "/data/adb/modules"
// 直接从用户选择的文件读取并解压
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir"))
val process = Runtime.getRuntime()
.exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir"))
context.contentResolver.openInputStream(uri)?.use { input ->
input.copyTo(process.outputStream)
@ -277,7 +282,11 @@ object ModuleModify {
}
} catch (e: Exception) {
Log.e("AllowlistRestore", context.getString(R.string.allowlist_restore_failed, ""), e)
Log.e(
"AllowlistRestore",
context.getString(R.string.allowlist_restore_failed, ""),
e
)
withContext(Dispatchers.Main) {
snackBarHost.showSnackbar(
context.getString(R.string.allowlist_restore_failed, e.message),
@ -292,11 +301,11 @@ object ModuleModify {
fun rememberModuleBackupLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
scope: CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
backupModules(context, snackBarHost, uri)
@ -309,8 +318,8 @@ object ModuleModify {
fun rememberModuleRestoreLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
): androidx.activity.result.ActivityResultLauncher<Intent> {
scope: CoroutineScope = rememberCoroutineScope()
): ActivityResultLauncher<Intent> {
var showRestoreDialog by remember { mutableStateOf(false) }
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
@ -330,7 +339,7 @@ object ModuleModify {
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
@ -353,11 +362,11 @@ object ModuleModify {
fun rememberAllowlistBackupLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
scope: CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
backupAllowlist(context, snackBarHost, uri)
@ -370,10 +379,14 @@ object ModuleModify {
fun rememberAllowlistRestoreLauncher(
context: Context,
snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
): androidx.activity.result.ActivityResultLauncher<Intent> {
scope: CoroutineScope = rememberCoroutineScope()
): ActivityResultLauncher<Intent> {
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
var allowlistRestoreConfirmResult by remember {
mutableStateOf<CompletableDeferred<Boolean>?>(
null
)
}
// 显示允许列表恢复确认对话框
AllowlistRestoreConfirmationDialog(
@ -391,7 +404,7 @@ object ModuleModify {
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.util.module
import android.content.Context
import android.content.Intent

View file

@ -1,9 +1,10 @@
package com.sukisu.ultra.ui.util
package com.sukisu.ultra.ui.util.module
import android.content.Context
import android.net.Uri
import android.util.Log
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.util.getRootShell
import java.io.File
import java.io.FileOutputStream

View file

@ -4,16 +4,11 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.system.Os
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives
import com.sukisu.ultra.getKernelVersion
@ -21,20 +16,14 @@ import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class HomeViewModel : ViewModel() {
companion object {
private const val TAG = "HomeViewModel"
private const val PREFS_NAME = "home_cache"
private const val KEY_SYSTEM_STATUS = "system_status"
private const val KEY_SYSTEM_INFO = "system_info"
private const val KEY_VERSION_INFO = "version_info"
private const val KEY_LAST_UPDATE = "last_update_time"
private const val KEY_ERROR_COUNT = "error_count"
private const val MAX_ERROR_COUNT = 2
}
// 系统状态
data class SystemStatus(
@ -60,7 +49,6 @@ class HomeViewModel : ViewModel() {
val suSFSVersion: String = "",
val suSFSVariant: String = "",
val suSFSFeatures: String = "",
val susSUMode: String = "",
val superuserCount: Int = 0,
val moduleCount: Int = 0,
val kpmModuleCount: Int = 0,
@ -69,9 +57,7 @@ class HomeViewModel : ViewModel() {
val zygiskImplement: String = ""
)
private val gson = Gson()
private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
// 状态变量
var systemStatus by mutableStateOf(SystemStatus())
private set
@ -98,199 +84,52 @@ class HomeViewModel : ViewModel() {
var showKpmInfo by mutableStateOf(false)
private set
private fun clearAllCache() {
try {
prefs.edit { clear() }
Log.i(TAG, "All cache cleared successfully")
} catch (e: Exception) {
Log.e(TAG, "Error clearing cache", e)
}
}
var isCoreDataLoaded by mutableStateOf(false)
private set
var isExtendedDataLoaded by mutableStateOf(false)
private set
var isRefreshing by mutableStateOf(false)
private set
private fun resetToDefaults() {
systemStatus = SystemStatus()
systemInfo = SystemInfo()
latestVersionInfo = LatestVersionInfo()
isSimpleMode = false
isKernelSimpleMode = false
isHideVersion = false
isHideOtherInfo = false
isHideSusfsStatus = false
isHideZygiskImplement = false
isHideLinkCard = false
showKpmInfo = false
}
private fun handleError(error: Exception, operation: String) {
Log.e(TAG, "Error in $operation", error)
// 数据刷新状态流,用于监听变化
private val _dataRefreshTrigger = MutableStateFlow(0L)
val dataRefreshTrigger: StateFlow<Long> = _dataRefreshTrigger
val errorCount = prefs.getInt(KEY_ERROR_COUNT, 0)
val newErrorCount = errorCount + 1
if (newErrorCount >= MAX_ERROR_COUNT) {
Log.w(TAG, "Too many errors ($newErrorCount), clearing cache and resetting")
clearAllCache()
resetToDefaults()
} else {
prefs.edit {
putInt(KEY_ERROR_COUNT, newErrorCount)
}
}
}
private fun String?.orSafe(default: String = ""): String {
return if (this.isNullOrBlank()) default else this
}
private fun <T, R> Pair<T?, R?>?.orSafe(default: Pair<T, R>): Pair<T, R> {
return if (this?.first == null || this.second == null) default else Pair(this.first!!, this.second!!)
}
private var loadingJobs = mutableListOf<Job>()
private var lastRefreshTime = 0L
private val refreshCooldown = 2000L
fun loadUserSettings(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false)
isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false)
isHideVersion = settingsPrefs.getBoolean("is_hide_version", false)
isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false)
isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false)
isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false)
isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false)
showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false)
} catch (e: Exception) {
handleError(e, "loadUserSettings")
}
val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false)
isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false)
isHideVersion = settingsPrefs.getBoolean("is_hide_version", false)
isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false)
isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false)
isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false)
isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false)
showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false)
}
}
fun initializeData() {
viewModelScope.launch {
try {
loadCachedData()
// 成功加载后重置错误计数
prefs.edit {
putInt(KEY_ERROR_COUNT, 0)
}
} catch(e: Exception) {
handleError(e, "initializeData")
}
}
}
fun loadCoreData() {
if (isCoreDataLoaded) return
private fun loadCachedData() {
try {
prefs.getString(KEY_SYSTEM_STATUS, null)?.let { statusJson ->
try {
val cachedStatus = gson.fromJson(statusJson, SystemStatus::class.java)
if (cachedStatus != null) {
systemStatus = cachedStatus
}
} catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid system status JSON, using defaults", e)
}
}
prefs.getString(KEY_SYSTEM_INFO, null)?.let { infoJson ->
try {
val cachedInfo = gson.fromJson(infoJson, SystemInfo::class.java)
if (cachedInfo != null) {
systemInfo = cachedInfo
}
} catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid system info JSON, using defaults", e)
}
}
prefs.getString(KEY_VERSION_INFO, null)?.let { versionJson ->
try {
val cachedVersion = gson.fromJson(versionJson, LatestVersionInfo::class.java)
if (cachedVersion != null) {
latestVersionInfo = cachedVersion
}
} catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid version info JSON, using defaults", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error loading cached data", e)
throw e
}
}
private suspend fun fetchAndSaveData() {
try {
fetchSystemStatus()
fetchSystemInfo()
withContext(Dispatchers.IO) {
prefs.edit {
putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus))
putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo))
putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
putInt(KEY_ERROR_COUNT, 0)
}
}
} catch (e: Exception) {
handleError(e, "fetchAndSaveData")
}
}
fun checkForUpdates(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val checkUpdate = settingsPrefs.getBoolean("check_update", true)
if (checkUpdate) {
val newVersionInfo = checkNewVersion()
latestVersionInfo = newVersionInfo
prefs.edit {
putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
}
}
} catch (e: Exception) {
handleError(e, "checkForUpdates")
}
}
}
fun refreshAllData(context: Context) {
viewModelScope.launch {
try {
fetchAndSaveData()
checkForUpdates(context)
} catch (e: Exception) {
handleError(e, "refreshAllData")
}
}
}
private suspend fun fetchSystemStatus() {
withContext(Dispatchers.IO) {
val job = viewModelScope.launch(Dispatchers.IO) {
try {
val kernelVersion = getKernelVersion()
val isManager = try {
Natives.becomeManager(ksuApp.packageName.orSafe("com.sukisu.ultra"))
} catch (e: Exception) {
Log.w(TAG, "Failed to become manager", e)
Natives.isManager
} catch (_: Exception) {
false
}
val ksuVersion = if (isManager) {
try {
Natives.version
} catch (e: Exception) {
Log.w(TAG, "Failed to get KSU version", e)
null
}
} else null
val ksuVersion = if (isManager) Natives.version else null
val fullVersion = try {
Natives.getFullVersion().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get full version", e)
Natives.getFullVersion()
} catch (_: Exception) {
"Unknown"
}
@ -309,8 +148,7 @@ class HomeViewModel : ViewModel() {
} else {
fullVersion
}
} catch (e: Exception) {
Log.w(TAG, "Failed to process full version", e)
} catch (_: Exception) {
fullVersion
}
} else {
@ -318,34 +156,24 @@ class HomeViewModel : ViewModel() {
}
val lkmMode = ksuVersion?.let {
try {
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) {
Natives.isLkmMode
} else null
} catch (e: Exception) {
Log.w(TAG, "Failed to get LKM mode", e)
null
}
if (kernelVersion.isGKI()) Natives.isLkmMode else null
}
val isRootAvailable = try {
rootAvailable()
} catch (e: Exception) {
Log.w(TAG, "Failed to check root availability", e)
} catch (_: Exception) {
false
}
val isKpmConfigured = try {
Natives.isKPMEnabled()
} catch (e: Exception) {
Log.w(TAG, "Failed to check KPM status", e)
} catch (_: Exception) {
false
}
val requireNewKernel = try {
isManager && Natives.requireNewKernel()
} catch (e: Exception) {
Log.w(TAG, "Failed to check kernel requirement", e)
} catch (_: Exception) {
false
}
@ -359,198 +187,321 @@ class HomeViewModel : ViewModel() {
isKpmConfigured = isKpmConfigured,
requireNewKernel = requireNewKernel
)
} catch (e: Exception) {
Log.e(TAG, "Error fetching system status", e)
throw e
isCoreDataLoaded = true
} catch (_: Exception) {
}
}
loadingJobs.add(job)
}
@SuppressLint("RestrictedApi")
private suspend fun fetchSystemInfo() {
withContext(Dispatchers.IO) {
fun loadExtendedData(context: Context) {
if (isExtendedDataLoaded) return
val job = viewModelScope.launch(Dispatchers.IO) {
try {
val uname = try {
Os.uname()
} catch (e: Exception) {
Log.w(TAG, "Failed to get uname", e)
null
}
// 分批加载
delay(50)
val kpmVersion = try {
getKpmVersion().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get kpm version", e)
"Unknown"
}
val suSFS = try {
getSuSFS().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS", e)
"Unknown"
}
var suSFSVersion = ""
var suSFSVariant = ""
var suSFSFeatures = ""
var susSUMode = ""
if (suSFS == "Supported") {
suSFSVersion = try {
getSuSFSVersion().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS version", e)
""
}
if (suSFSVersion.isNotEmpty()) {
suSFSVariant = try {
getSuSFSVariant().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS variant", e)
""
}
suSFSFeatures = try {
getSuSFSFeatures().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS features", e)
""
}
val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
if (isSUS_SU) {
susSUMode = try {
susfsSUS_SU_Mode()
} catch (e: Exception) {
Log.w(TAG, "Failed to get SUS SU mode", e)
""
}
}
}
}
// 获取动态管理器状态和管理器列表
val dynamicSignConfig = try {
Natives.getDynamicManager()
} catch (e: Exception) {
Log.w(TAG, "Failed to get dynamic manager config", e)
null
}
val isDynamicSignEnabled = try {
dynamicSignConfig?.isValid() == true
} catch (e: Exception) {
Log.w(TAG, "Failed to check dynamic manager validity", e)
false
}
val managersList = if (isDynamicSignEnabled) {
try {
Natives.getManagersList()
} catch (e: Exception) {
Log.w(TAG, "Failed to get managers list", e)
null
}
} else {
null
}
val deviceModel = try {
getDeviceModel().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get device model", e)
"Unknown"
}
val managerVersion = try {
getManagerVersion(ksuApp.applicationContext).orSafe(Pair("Unknown", 0L))
} catch (e: Exception) {
Log.w(TAG, "Failed to get manager version", e)
Pair("Unknown", 0L)
}
val seLinuxStatus = try {
getSELinuxStatus(context).orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SELinux status", e)
"Unknown"
}
val superuserCount = try {
getSuperuserCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get superuser count", e)
0
}
val moduleCount = try {
getModuleCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get module count", e)
0
}
val kpmModuleCount = try {
getKpmModuleCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get kpm module count", e)
0
}
val zygiskImplement = try {
getZygiskImplement().orSafe("None")
} catch (e: Exception) {
Log.w(TAG, "Failed to get Zygisk implement", e)
"None"
}
systemInfo = SystemInfo(
kernelRelease = uname?.release.orSafe("Unknown"),
androidVersion = Build.VERSION.RELEASE.orSafe("Unknown"),
deviceModel = deviceModel,
managerVersion = managerVersion,
seLinuxStatus = seLinuxStatus,
kpmVersion = kpmVersion,
suSFSStatus = suSFS,
suSFSVersion = suSFSVersion,
suSFSVariant = suSFSVariant,
suSFSFeatures = suSFSFeatures,
susSUMode = susSUMode,
superuserCount = superuserCount,
moduleCount = moduleCount,
kpmModuleCount = kpmModuleCount,
managersList = managersList,
isDynamicSignEnabled = isDynamicSignEnabled,
zygiskImplement = zygiskImplement
val basicInfo = loadBasicSystemInfo(context)
systemInfo = systemInfo.copy(
kernelRelease = basicInfo.first,
androidVersion = basicInfo.second,
deviceModel = basicInfo.third,
managerVersion = basicInfo.fourth,
seLinuxStatus = basicInfo.fifth
)
} catch (e: Exception) {
Log.e(TAG, "Error fetching system info", e)
throw e
delay(100)
// 加载模块信息
if (!isSimpleMode) {
val moduleInfo = loadModuleInfo()
systemInfo = systemInfo.copy(
kpmVersion = moduleInfo.first,
superuserCount = moduleInfo.second,
moduleCount = moduleInfo.third,
kpmModuleCount = moduleInfo.fourth,
zygiskImplement = moduleInfo.fifth
)
}
delay(100)
// 加载SuSFS信息
if (!isHideSusfsStatus) {
val suSFSInfo = loadSuSFSInfo()
systemInfo = systemInfo.copy(
suSFSStatus = suSFSInfo.first,
suSFSVersion = suSFSInfo.second,
suSFSVariant = suSFSInfo.third,
suSFSFeatures = suSFSInfo.fourth,
)
}
delay(100)
// 加载管理器列表
val managerInfo = loadManagerInfo()
systemInfo = systemInfo.copy(
managersList = managerInfo.first,
isDynamicSignEnabled = managerInfo.second
)
isExtendedDataLoaded = true
} catch (_: Exception) {
// 静默处理错误
}
}
loadingJobs.add(job)
}
fun refreshData(context: Context, forceRefresh: Boolean = false) {
val currentTime = System.currentTimeMillis()
// 如果不是强制刷新,检查冷却时间
if (!forceRefresh && currentTime - lastRefreshTime < refreshCooldown) {
return
}
lastRefreshTime = currentTime
viewModelScope.launch {
isRefreshing = true
try {
// 取消正在进行的加载任务
loadingJobs.forEach { it.cancel() }
loadingJobs.clear()
// 重置状态
isCoreDataLoaded = false
isExtendedDataLoaded = false
// 触发数据刷新状态流
_dataRefreshTrigger.value = currentTime
// 重新加载用户设置
loadUserSettings(context)
// 重新加载核心数据
loadCoreData()
delay(100)
// 重新加载扩展数据
loadExtendedData(context)
// 检查更新
val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val checkUpdate = settingsPrefs.getBoolean("check_update", true)
if (checkUpdate) {
try {
val newVersionInfo = withContext(Dispatchers.IO) {
checkNewVersion()
}
latestVersionInfo = newVersionInfo
} catch (_: Exception) {
}
}
} catch (_: Exception) {
// 静默处理错误
} finally {
isRefreshing = false
}
}
}
private fun getDeviceInfo(): String {
return try {
var manufacturer = Build.MANUFACTURER.orSafe("Unknown")
manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1)
// 手动触发刷新(下拉刷新使用)
fun onPullRefresh(context: Context) {
refreshData(context, forceRefresh = true)
}
val brand = Build.BRAND.orSafe("")
if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) {
manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1)
// 自动刷新数据(当检测到变化时)
fun autoRefreshIfNeeded(context: Context) {
viewModelScope.launch {
// 检查是否需要刷新数据
val needsRefresh = checkIfDataNeedsRefresh()
if (needsRefresh) {
refreshData(context)
}
}
}
private suspend fun checkIfDataNeedsRefresh(): Boolean {
return withContext(Dispatchers.IO) {
try {
// 检查KSU状态是否发生变化
val currentKsuVersion = try {
if (Natives.isManager) {
Natives.version
} else null
} catch (_: Exception) {
null
}
// 如果KSU版本发生变化需要刷新
if (currentKsuVersion != systemStatus.ksuVersion) {
return@withContext true
}
// 检查模块数量是否发生变化
val currentModuleCount = try {
getModuleCount()
} catch (_: Exception) {
systemInfo.moduleCount
}
if (currentModuleCount != systemInfo.moduleCount) {
return@withContext true
}
false
} catch (_: Exception) {
false
}
}
}
private suspend fun loadBasicSystemInfo(context: Context): Tuple5<String, String, String, Pair<String, Long>, String> {
return withContext(Dispatchers.IO) {
val uname = try {
Os.uname()
} catch (_: Exception) {
null
}
val model = Build.MODEL.orSafe("")
if (model.isNotEmpty()) {
manufacturer += " $model "
val deviceModel = try {
getDeviceModel()
} catch (_: Exception) {
"Unknown"
}
manufacturer
} catch (e: Exception) {
Log.w(TAG, "Failed to get device info", e)
"Unknown Device"
val managerVersion = try {
getManagerVersion(context)
} catch (_: Exception) {
Pair("Unknown", 0L)
}
val seLinuxStatus = try {
getSELinuxStatus(ksuApp.applicationContext)
} catch (_: Exception) {
"Unknown"
}
Tuple5(
uname?.release ?: "Unknown",
Build.VERSION.RELEASE ?: "Unknown",
deviceModel,
managerVersion,
seLinuxStatus
)
}
}
private suspend fun loadModuleInfo(): Tuple5<String, Int, Int, Int, String> {
return withContext(Dispatchers.IO) {
val kpmVersion = try {
getKpmVersion()
} catch (_: Exception) {
"Unknown"
}
val superuserCount = try {
getSuperuserCount()
} catch (_: Exception) {
0
}
val moduleCount = try {
getModuleCount()
} catch (_: Exception) {
0
}
val kpmModuleCount = try {
getKpmModuleCount()
} catch (_: Exception) {
0
}
val zygiskImplement = try {
getZygiskImplement()
} catch (_: Exception) {
"None"
}
Tuple5(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement)
}
}
private suspend fun loadSuSFSInfo(): Tuple4<String, String, String, String> {
return withContext(Dispatchers.IO) {
val suSFS = try {
val rawFeature = getSuSFSFeatures()
if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) {
"Supported"
} else {
rawFeature
}
} catch (_: Exception) {
"Unknown"
}
if (suSFS != "Supported") {
return@withContext Tuple4(suSFS, "", "", "")
}
val suSFSVersion = try {
getSuSFSVersion()
} catch (_: Exception) {
""
}
if (suSFSVersion.isEmpty()) {
return@withContext Tuple4(suSFS, "", "", "")
}
val suSFSVariant = try {
getSuSFSVariant()
} catch (_: Exception) {
""
}
val suSFSFeatures = try {
getSuSFSFeatures()
} catch (_: Exception) {
""
}
Tuple4(suSFS, suSFSVersion, suSFSVariant, suSFSFeatures)
}
}
private suspend fun loadManagerInfo(): Pair<Natives.ManagersList?, Boolean> {
return withContext(Dispatchers.IO) {
val dynamicSignConfig = try {
Natives.getDynamicManager()
} catch (_: Exception) {
null
}
val isDynamicSignEnabled = try {
dynamicSignConfig?.isValid() == true
} catch (_: Exception) {
false
}
val managersList = if (isDynamicSignEnabled) {
try {
Natives.getManagersList()
} catch (_: Exception) {
null
}
} else {
null
}
Pair(managersList, isDynamicSignEnabled)
}
}
@ -560,10 +511,10 @@ class HomeViewModel : ViewModel() {
val systemProperties = Class.forName("android.os.SystemProperties")
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
val marketNameKeys = listOf(
"ro.product.marketname", // Xiaomi
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
"ro.vivo.market.name", // Vivo
"ro.config.marketing_name" // Huawei
"ro.product.marketname",
"ro.vendor.oplus.market.name",
"ro.vivo.market.name",
"ro.config.marketing_name"
)
var result = getDeviceInfo()
for (key in marketNameKeys) {
@ -573,26 +524,67 @@ class HomeViewModel : ViewModel() {
result = marketName
break
}
} catch (e: Exception) {
Log.w(TAG, "Failed to get market name for key: $key", e)
} catch (_: Exception) {
}
}
result
} catch (e: Exception) {
Log.w(TAG, "Error getting device model", e)
} catch (
_: Exception) {
getDeviceInfo()
}
}
private fun getDeviceInfo(): String {
return try {
var manufacturer = Build.MANUFACTURER ?: "Unknown"
manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1)
val brand = Build.BRAND ?: ""
if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) {
manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1)
}
val model = Build.MODEL ?: ""
if (model.isNotEmpty()) {
manufacturer += " $model "
}
manufacturer
} catch (_: Exception) {
"Unknown Device"
}
}
private fun getManagerVersion(context: Context): Pair<String, Long> {
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo)
val versionName = packageInfo.versionName.orSafe("Unknown")
val versionName = packageInfo.versionName ?: "Unknown"
Pair(versionName, versionCode)
} catch (e: Exception) {
Log.w(TAG, "Error getting manager version", e)
} catch (_: Exception) {
Pair("Unknown", 0L)
}
}
data class Tuple5<T1, T2, T3, T4, T5>(
val first: T1,
val second: T2,
val third: T3,
val fourth: T4,
val fifth: T5
)
data class Tuple4<T1, T2, T3, T4>(
val first: T1,
val second: T2,
val third: T3,
val fourth: T4
)
override fun onCleared() {
super.onCleared()
loadingJobs.forEach { it.cancel() }
loadingJobs.clear()
}
}

View file

@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.ModuleVerificationManager
import com.sukisu.ultra.ui.util.module.ModuleVerificationManager
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject

View file

@ -3,9 +3,9 @@ package com.sukisu.ultra.ui.viewmodel
import android.content.*
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.graphics.drawable.Drawable
import android.os.IBinder
import android.os.Parcelable
import android.os.SystemClock
import android.util.Log
import androidx.compose.runtime.*
import androidx.core.content.edit
@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModel
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.*
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@ -26,8 +26,8 @@ import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import com.sukisu.zako.IKsuInterface
// 应用分类
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
@ -35,13 +35,10 @@ enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
companion object {
fun fromPersistKey(key: String): AppCategory {
return entries.find { it.persistKey == key } ?: ALL
}
fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL
}
}
// 排序方式
enum class SortType(val displayNameRes: Int, val persistKey: String) {
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
@ -52,20 +49,24 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) {
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
companion object {
fun fromPersistKey(key: String): SortType {
return entries.find { it.persistKey == key } ?: NAME_ASC
}
fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC
}
}
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
class SuperUserViewModel : ViewModel() {
companion object {
private const val TAG = "SuperUserViewModel"
private val appsLock = Any()
var apps by mutableStateOf<List<AppInfo>>(emptyList())
var appGroups by mutableStateOf<List<AppGroup>>(emptyList())
@JvmStatic
fun getAppIconDrawable(context: Context, packageName: String): Drawable? {
val appList = synchronized(appsLock) { apps }
return appList.find { it.packageName == packageName }
?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
}
private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category"
@ -82,31 +83,29 @@ class SuperUserViewModel : ViewModel() {
val packageInfo: PackageInfo,
val profile: Natives.Profile?,
) : Parcelable {
val packageName: String
get() = packageInfo.packageName
val uid: Int
get() = packageInfo.applicationInfo!!.uid
val packageName: String get() = packageInfo.packageName
val uid: Int get() = packageInfo.applicationInfo!!.uid
}
val allowSu: Boolean
get() = profile != null && profile.allowSu
@Parcelize
data class AppGroup(
val uid: Int,
val apps: List<AppInfo>,
val profile: Natives.Profile?
) : Parcelable {
val mainApp: AppInfo get() = apps.first()
val packageNames: List<String> get() = apps.map { it.packageName }
val allowSu: Boolean get() = profile?.allowSu == true
val userName: String? get() = Natives.getUserName(uid)
val hasCustomProfile: Boolean
get() {
if (profile == null) {
return false
}
return if (profile.allowSu) {
!profile.rootUseDefault
} else {
!profile.nonRootUseDefault
}
}
get() = profile?.let {
if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault
} ?: false
}
private val appProcessingThreadPool = ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
LinkedBlockingQueue()
) { runnable ->
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
@ -116,65 +115,40 @@ class SuperUserViewModel : ViewModel() {
}.asCoroutineDispatcher()
private val appListMutex = Mutex()
private val configChangeListeners = mutableSetOf<(String) -> Unit>()
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
var search by mutableStateOf("")
var showSystemApps by mutableStateOf(loadShowSystemApps())
var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false))
private set
var selectedCategory by mutableStateOf(loadSelectedCategory())
private set
var currentSortType by mutableStateOf(loadCurrentSortType())
private set
var isRefreshing by mutableStateOf(false)
private set
// 批量操作相关状态
var showBatchActions by mutableStateOf(false)
internal set
var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set
// 加载进度状态
var loadingProgress by mutableFloatStateOf(0f)
private set
var loadingMessage by mutableStateOf("")
private set
/**
* 从SharedPreferences加载显示系统应用设置
*/
private fun loadShowSystemApps(): Boolean {
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
}
/**
* 从SharedPreferences加载选择的应用分类
*/
private fun loadSelectedCategory(): AppCategory {
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey)
?: AppCategory.ALL.persistKey
return AppCategory.fromPersistKey(categoryKey)
}
/**
* 从SharedPreferences加载当前排序方式
*/
private fun loadCurrentSortType(): SortType {
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey)
?: SortType.NAME_ASC.persistKey
return SortType.fromPersistKey(sortKey)
}
/**
* 更新显示系统应用设置并保存到SharedPreferences
*/
fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue
saveShowSystemApps(newValue)
prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) }
notifyAppListChanged()
}
@ -184,88 +158,21 @@ class SuperUserViewModel : ViewModel() {
apps = currentApps
}
/**
* 更新选择的应用分类并保存到SharedPreferences
*/
fun updateSelectedCategory(newCategory: AppCategory) {
selectedCategory = newCategory
saveSelectedCategory(newCategory)
prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) }
}
/**
* 更新当前排序方式并保存到SharedPreferences
*/
fun updateCurrentSortType(newSortType: SortType) {
currentSortType = newSortType
saveCurrentSortType(newSortType)
prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) }
}
/**
* 保存显示系统应用设置到SharedPreferences
*/
private fun saveShowSystemApps(value: Boolean) {
prefs.edit {
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
}
Log.d(TAG, "Saved show system apps: $value")
}
/**
* 保存选择的应用分类到SharedPreferences
*/
private fun saveSelectedCategory(category: AppCategory) {
prefs.edit {
putString(KEY_SELECTED_CATEGORY, category.persistKey)
}
Log.d(TAG, "Saved selected category: ${category.persistKey}")
}
/**
* 保存当前排序方式到SharedPreferences
*/
private fun saveCurrentSortType(sortType: SortType) {
prefs.edit {
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
}
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
}
private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> {
when {
it.allowSu -> 0
it.hasCustomProfile -> 1
else -> 2
}
}.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
apps.sortedWith(comparator).also {
isRefreshing = false
}
}
val appList by derivedStateOf {
val filtered = sortedList.filter {
it.label.contains(search, true) || it.packageName.contains(
search,
true
) || HanziToPinyin.getInstance()
.toPinyinString(it.label).contains(search, true)
}.filter {
it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
}
filtered
}
// 切换批量操作模式
fun toggleBatchMode() {
showBatchActions = !showBatchActions
if (!showBatchActions) {
clearSelection()
}
if (!showBatchActions) clearSelection()
}
// 切换应用选择状态
fun toggleAppSelection(packageName: String) {
selectedApps = if (selectedApps.contains(packageName)) {
selectedApps - packageName
@ -274,35 +181,14 @@ class SuperUserViewModel : ViewModel() {
}
}
// 清除所有选择
fun clearSelection() {
selectedApps = emptySet()
}
// 批量更新权限
suspend fun updateBatchPermissions(allowSu: Boolean) {
selectedApps.forEach { packageName ->
val app = apps.find { it.packageName == packageName }
app?.let {
val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
updateAppProfileLocally(packageName, updatedProfile)
notifyConfigChange(packageName)
}
}
}
clearSelection()
showBatchActions = false
refreshAppConfigurations()
}
// 批量更新权限和umount模块设置
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
selectedApps.forEach { packageName ->
val app = apps.find { it.packageName == packageName }
app?.let {
val profile = Natives.getAppProfile(packageName, it.uid)
apps.find { it.packageName == packageName }?.let { app ->
val profile = Natives.getAppProfile(packageName, app.uid)
val updatedProfile = profile.copy(
allowSu = allowSu,
umountModules = umountModules ?: profile.umountModules,
@ -319,7 +205,6 @@ class SuperUserViewModel : ViewModel() {
refreshAppConfigurations()
}
// 更新本地应用配置
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
appListMutex.tryLock().let { locked ->
if (locked) {
@ -327,9 +212,7 @@ class SuperUserViewModel : ViewModel() {
apps = apps.map { app ->
if (app.packageName == packageName) {
app.copy(profile = updatedProfile)
} else {
app
}
} else app
}
} finally {
appListMutex.unlock()
@ -348,15 +231,11 @@ class SuperUserViewModel : ViewModel() {
}
}
/**
* 刷新应用配置状态
*/
suspend fun refreshAppConfigurations() {
withContext(appProcessingThreadPool) {
supervisorScope {
val currentApps = apps.toList()
val batches = currentApps.chunked(BATCH_SIZE)
loadingProgress = 0f
val updatedApps = batches.mapIndexed { batchIndex, batch ->
@ -370,59 +249,45 @@ class SuperUserViewModel : ViewModel() {
app
}
}
val progress = (batchIndex + 1).toFloat() / batches.size
loadingProgress = progress
loadingProgress = (batchIndex + 1).toFloat() / batches.size
batchResult
}
}.awaitAll().flatten()
appListMutex.withLock {
apps = updatedApps
}
appListMutex.withLock { apps = updatedApps }
loadingProgress = 1f
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
}
}
}
private var serviceConnection: ServiceConnection? = null
private suspend fun connectKsuService(
onDisconnect: () -> Unit = {}
): IBinder? = suspendCoroutine { continuation ->
val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect()
serviceConnection = null
private suspend fun connectKsuService(onDisconnect: () -> Unit = {}): IBinder? =
suspendCoroutine { continuation ->
val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect()
serviceConnection = null
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
continuation.resume(binder)
}
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
continuation.resume(binder)
serviceConnection = connection
val intent = Intent(ksuApp, KsuService::class.java)
try {
val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask(
intent, Shell.EXECUTOR, connection
)
task?.let { Shell.getShell().execTask(it) }
} catch (e: Exception) {
Log.e(TAG, "Failed to bind KsuService", e)
continuation.resume(null)
}
}
serviceConnection = connection
val intent = Intent(ksuApp, KsuService::class.java)
try {
val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask(
intent,
Shell.EXECUTOR,
connection
)
task?.let { Shell.getShell().execTask(it) }
} catch (e: Exception) {
Log.e(TAG, "Failed to bind KsuService", e)
continuation.resume(null)
}
}
private fun stopKsuService() {
serviceConnection?.let { connection ->
serviceConnection?.let {
try {
val intent = Intent(ksuApp, KsuService::class.java)
com.topjohnwu.superuser.ipc.RootService.stop(intent)
@ -437,60 +302,77 @@ class SuperUserViewModel : ViewModel() {
isRefreshing = true
loadingProgress = 0f
val result = connectKsuService {
Log.w(TAG, "KsuService disconnected")
}
if (result == null) {
Log.e(TAG, "Failed to connect to KsuService")
isRefreshing = false
return
}
val binder = connectKsuService() ?: run { isRefreshing = false; return }
withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime()
val allPackages = IKsuInterface.Stub.asInterface(binder)
val total = allPackages.packageCount
val pageSize = 100
val result = mutableListOf<AppInfo>()
try {
val service = KsuService.Stub.asInterface(result)
val allPackages = service?.getPackages(0)
var start = 0
while (start < total) {
val page = allPackages.getPackages(start, pageSize)
if (page.isEmpty()) break
withContext(Dispatchers.Main) {
stopKsuService()
result += page.mapNotNull { packageInfo ->
packageInfo.applicationInfo?.let { appInfo ->
AppInfo(
label = appInfo.loadLabel(pm).toString(),
packageInfo = packageInfo,
profile = Natives.getAppProfile(packageInfo.packageName, appInfo.uid)
)
}
}
loadingProgress = 0.3f
val packages = allPackages?.list ?: emptyList()
apps = packages.map { packageInfo ->
val appInfo = packageInfo.applicationInfo!!
val uid = appInfo.uid
val profile = Natives.getAppProfile(packageInfo.packageName, uid)
AppInfo(
label = appInfo.loadLabel(pm).toString(),
packageInfo = packageInfo,
profile = profile,
)
}.filter { it.packageName != ksuApp.packageName }
loadingProgress = 1f
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}")
} catch (e: Exception) {
Log.e(TAG, "Error fetching app list", e)
withContext(Dispatchers.Main) {
stopKsuService()
}
} finally {
isRefreshing = false
loadingProgress = 0f
loadingMessage = ""
start += page.size
loadingProgress = start.toFloat() / total
}
stopKsuService()
appListMutex.withLock {
val filteredApps = result.filter { it.packageName != ksuApp.packageName }
apps = filteredApps
appGroups = groupAppsByUid(filteredApps)
}
loadingProgress = 1f
}
isRefreshing = false
}
val appGroupList by derivedStateOf {
appGroups.filter { group ->
group.apps.any { app ->
app.label.contains(search, true) ||
app.packageName.contains(search, true) ||
HanziToPinyin.getInstance().toPinyinString(app.label).contains(search, true)
}
}.filter { group ->
group.uid == 2000 || showSystemApps ||
group.apps.any { it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 }
}
}
/**
* 清理资源
*/
private fun groupAppsByUid(appList: List<AppInfo>): List<AppGroup> {
return appList.groupBy { it.uid }
.map { (uid, apps) ->
val sortedApps = apps.sortedBy { it.label }
val profile = apps.firstOrNull()?.let { Natives.getAppProfile(it.packageName, uid) }
AppGroup(uid = uid, apps = sortedApps, profile = profile)
}
.sortedWith(
compareBy<AppGroup> {
when {
it.allowSu -> 0
it.hasCustomProfile -> 1
else -> 2
}
}.thenBy(Collator.getInstance(Locale.getDefault())) {
it.userName?.takeIf { name -> name.isNotBlank() } ?: it.uid.toString()
}.thenBy(Collator.getInstance(Locale.getDefault())) { it.mainApp.label }
)
}
override fun onCleared() {
super.onCleared()
try {

View file

@ -0,0 +1,47 @@
package com.sukisu.ultra.ui.webui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.LruCache;
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel;
public class AppIconUtil {
// Limit cache size to 200 icons
private static final int CACHE_SIZE = 200;
private static final LruCache<String, Bitmap> iconCache = new LruCache<>(CACHE_SIZE);
public static synchronized Bitmap loadAppIconSync(Context context, String packageName, int sizePx) {
Bitmap cached = iconCache.get(packageName);
if (cached != null) return cached;
try {
Drawable drawable = SuperUserViewModel.getAppIconDrawable(context, packageName);
if (drawable == null) {
return null;
}
Bitmap raw = drawableToBitmap(drawable, sizePx);
Bitmap icon = Bitmap.createScaledBitmap(raw, sizePx, sizePx, true);
if (raw != icon) raw.recycle();
iconCache.put(packageName, icon);
return icon;
} catch (Exception e) {
return null;
}
}
private static Bitmap drawableToBitmap(Drawable drawable, int size) {
if (drawable instanceof BitmapDrawable) return ((BitmapDrawable) drawable).getBitmap();
int width = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : size;
int height = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : size;
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bmp);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bmp;
}
}

View file

@ -1,7 +1,6 @@
package com.sukisu.ultra.ui.webui
import android.content.ServiceConnection
import android.content.pm.PackageInfo
import android.util.Log
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.model.IProvider
@ -18,7 +17,7 @@ class KsuLibSuProvider : IProvider {
override fun isAvailable() = true
override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
override suspend fun isAuthorized() = Natives.isManager
private val serviceIntent
get() = PlatformIntent(
@ -54,19 +53,4 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
Log.e("KsuLibSu", "Failed to initialize platform", e)
return@withContext false
}
}
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
try {
val packages = mutableListOf<PackageInfo>()
val userInfos = userManager.getUsers()
for (userInfo in userInfos) {
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
}
packages
} catch (e: Exception) {
catch(e)
packageManager.getInstalledPackages(0, userManager.myUserId)
}
}

View file

@ -11,18 +11,23 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.webkit.WebViewAssetLoader
import com.dergoogler.mmrl.platform.model.ModId
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.sukisu.ultra.ui.util.createRootShell
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import kotlinx.coroutines.launch
import java.io.File
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
private val rootShell by lazy { createRootShell(true) }
private val superUserViewModel: SuperUserViewModel by viewModels()
private var webView = null as WebView?
override fun onCreate(savedInstanceState: Bundle?) {
@ -35,6 +40,10 @@ class WebUIActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
superUserViewModel.fetchAppList()
}
val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return }
val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@ -64,7 +73,21 @@ class WebUIActivity : ComponentActivity() {
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url)
val url = request.url
// Handle ksu://icon/[packageName] to serve app icon via WebView
if (url.scheme.equals("ksu", ignoreCase = true) && url.host.equals("icon", ignoreCase = true)) {
val packageName = url.path?.substring(1)
if (!packageName.isNullOrEmpty()) {
val icon = AppIconUtil.loadAppIconSync(this@WebUIActivity, packageName, 512)
if (icon != null) {
val stream = java.io.ByteArrayOutputStream()
icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream)
val inputStream = java.io.ByteArrayInputStream(stream.toByteArray())
return WebResourceResponse("image/png", null, inputStream)
}
}
}
return webViewAssetLoader.shouldInterceptRequest(url)
}
}

View file

@ -13,6 +13,7 @@ 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.model.WebUIConfig
import com.dergoogler.mmrl.webui.screen.WebUIScreen
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
import com.sukisu.ultra.BuildConfig
@ -95,6 +96,13 @@ class WebUIXActivity : ComponentActivity() {
userAgentString = userAgent
)
// idk why webuix not allow root impl change webuiConfig
// so we use magic to force exitConfirm shutdown
val field = WebUIConfig::class.java.getDeclaredField("exitConfirm")
field.isAccessible = true
field.set(options.config, false)
field.isAccessible = false
WebUIScreen(
webView = webView,
options = options,

View file

@ -1,17 +1,20 @@
package com.sukisu.ultra.ui.webui
import android.app.Activity
import android.content.pm.ApplicationInfo
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.dergoogler.mmrl.webui.interfaces.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.util.*
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
@ -138,7 +141,7 @@ class WebViewInterface(
completableFuture.thenAccept { result ->
val emitExitCode =
"(function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
$$"(function() { try { $${callbackFunc}.emit('exit', $${result.code}); } catch(e) { console.error(`emitExit error: ${e}`); } })();"
webView.post {
webView.evaluateJavascript(emitExitCode, null)
}
@ -203,6 +206,56 @@ class WebViewInterface(
return currentModuleInfo.toString()
}
@JavascriptInterface
fun listPackages(type: String): String {
val packageNames = SuperUserViewModel.apps
.filter { appInfo ->
val flags = appInfo.packageInfo.applicationInfo?.flags ?: 0
when (type.lowercase()) {
"system" -> (flags and ApplicationInfo.FLAG_SYSTEM) != 0
"user" -> (flags and ApplicationInfo.FLAG_SYSTEM) == 0
else -> true
}
}
.map { it.packageName }
.sorted()
val jsonArray = JSONArray()
for (pkgName in packageNames) {
jsonArray.put(pkgName)
}
return jsonArray.toString()
}
@JavascriptInterface
fun getPackagesInfo(packageNamesJson: String): String {
val packageNames = JSONArray(packageNamesJson)
val jsonArray = JSONArray()
val appMap = SuperUserViewModel.apps.associateBy { it.packageName }
for (i in 0 until packageNames.length()) {
val pkgName = packageNames.getString(i)
val appInfo = appMap[pkgName]
if (appInfo != null) {
val pkg = appInfo.packageInfo
val app = pkg.applicationInfo
val obj = JSONObject()
obj.put("packageName", pkg.packageName)
obj.put("versionName", pkg.versionName ?: "")
obj.put("versionCode", PackageInfoCompat.getLongVersionCode(pkg))
obj.put("appLabel", appInfo.label)
obj.put("isSystem", if (app != null) ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) else JSONObject.NULL)
obj.put("uid", app?.uid ?: JSONObject.NULL)
jsonArray.put(obj)
} else {
val obj = JSONObject()
obj.put("packageName", pkgName)
obj.put("error", "Package not found or inaccessible")
jsonArray.put(obj)
}
}
return jsonArray.toString()
}
// =================== KPM支持 =============================
@JavascriptInterface

View file

@ -1,29 +0,0 @@
package io.sukisu.ultra;
import java.util.ArrayList;
import com.sukisu.ultra.ui.util.KsuCli;
public class UltraShellHelper {
public static String runCmd(String cmds) {
StringBuilder sb = new StringBuilder();
for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL()
.newJob()
.add(cmds)
.to(new ArrayList<>(), null)
.exec()
.getOut()) {
sb.append(str).append("\n");
}
return sb.toString();
}
public static boolean isPathExists(String path) {
String result = runCmd("test -f '" + path + "' && echo 'exists'");
return result.contains("exists");
}
public static void CopyFileTo(String path, String target) {
runCmd("cp -f '" + path + "' '" + target + "' 2>&1");
}
}

View file

@ -1,23 +0,0 @@
package io.sukisu.ultra;
import static com.sukisu.ultra.ui.util.KsuCliKt.*;
import android.annotation.SuppressLint;
public class UltraToolInstall {
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd";
@SuppressLint("SetWorldReadable")
public static void tryToInstall() {
String kpmmgrPath = getKpmmgrPath();
if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
}
String SuSFSDaemonPath = getSuSFSDaemonPath();
if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
}
}
}

View file

@ -1,20 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import androidx.compose.animation.*
import androidx.compose.runtime.Composable
object AnimatedBottomBar {
@Composable
fun AnimatedBottomBarWrapper(
showBottomBar: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = showBottomBar,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
) {
content()
}
}
}

View file

@ -1,90 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object AppData {
object DataRefreshManager {
// 私有状态流
private val _superuserCount = MutableStateFlow(0)
private val _moduleCount = MutableStateFlow(0)
private val _kpmModuleCount = MutableStateFlow(0)
// 公开的只读状态流
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
/**
* 刷新所有数据计数
*/
fun refreshData() {
_superuserCount.value = getSuperuserCountUse()
_moduleCount.value = getModuleCountUse()
_kpmModuleCount.value = getKpmModuleCountUse()
}
}
/**
* 获取超级用户应用计数
*/
fun getSuperuserCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getSuperuserCount()
} catch (_: Exception) {
0
}
}
/**
* 获取模块计数
*/
fun getModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM模块计数
*/
fun getKpmModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
val kpmVersion = getKpmVersionUse()
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0
getKpmModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM版本
*/
fun getKpmVersionUse(): String {
return try {
if (!rootAvailable()) return ""
val version = getKpmVersion()
version.ifEmpty { "" }
} catch (e: Exception) {
"Error: ${e.message}"
}
}
/**
* 检查是否是完整功能模式
*/
fun isFullFeatured(packageName: String): Boolean {
val isManager = Natives.becomeManager(packageName)
return isManager && !Natives.requireNewKernel() && rootAvailable()
}
}

View file

@ -1,46 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.sukisu.ultra.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
object DataRefreshUtils {
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
scope.launch(Dispatchers.IO) {
while (isActive) {
DataRefreshManager.refreshData()
delay(5000)
}
}
}
fun startSettingsMonitorCoroutine(
scope: LifecycleCoroutineScope,
activity: MainActivity,
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
) {
scope.launch(Dispatchers.IO) {
while (isActive) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
delay(1000)
}
}
}
fun refreshData(scope: LifecycleCoroutineScope) {
scope.launch {
DataRefreshManager.refreshData()
}
}
}

View file

@ -1,24 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
object DisplayUtils {
fun applyCustomDpi(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try {
val resources = context.resources
val metrics = resources.displayMetrics
metrics.density = customDpi / 160f
@Suppress("DEPRECATION")
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View file

@ -1,48 +0,0 @@
package zako.zako.zako.zakoui.activity.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import java.util.*
object LocaleUtils {
@SuppressLint("ObsoleteSdkInt")
fun applyLanguageSetting(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
fun applyLocale(context: Context): Context {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var newContext = context
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
newContext = context.createConfigurationContext(config)
}
return newContext
}
}

View file

@ -1,7 +1,9 @@
package zako.zako.zako.zakoui.screen
package zako.zako.zako.zakoui.screen.kernelFlash
import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
@ -27,6 +29,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@ -39,9 +42,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.zakoui.flash.FlashState
import zako.zako.zako.zakoui.flash.HorizonKernelState
import zako.zako.zako.zakoui.flash.HorizonKernelWorker
import zako.zako.zako.zakoui.screen.kernelFlash.state.FlashState
import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelState
import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelWorker
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@ -73,6 +76,12 @@ fun KernelFlashScreen(
kpmUndoPatch: Boolean = false
) {
val context = LocalContext.current
val shouldAutoExit = remember {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.getBoolean("auto_exit_after_flash", false)
}
val scrollState = rememberScrollState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
@ -105,6 +114,16 @@ fun KernelFlashScreen(
val onFlashComplete = {
showFloatAction = true
KernelFlashStateHolder.isFlashing = false
// 如果需要自动退出延迟1.5秒后退出
if (shouldAutoExit) {
scope.launch {
delay(1500)
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
(context as? ComponentActivity)?.finish()
}
}
}
// 开始刷写
@ -165,6 +184,19 @@ fun KernelFlashScreen(
}
}
DisposableEffect(shouldAutoExit) {
onDispose {
if (shouldAutoExit) {
KernelFlashStateHolder.currentState = null
KernelFlashStateHolder.currentUri = null
KernelFlashStateHolder.currentSlot = null
KernelFlashStateHolder.currentKpmPatchEnabled = false
KernelFlashStateHolder.currentKpmUndoPatch = false
KernelFlashStateHolder.isFlashing = false
}
}
}
BackHandler(enabled = true) {
onBack()
}

View file

@ -1,4 +1,4 @@
package com.sukisu.ultra.ui.component
package zako.zako.zako.zakoui.screen.kernelFlash.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable

View file

@ -1,4 +1,4 @@
package zako.zako.zako.zakoui.flash
package zako.zako.zako.zakoui.screen.kernelFlash.state
import android.annotation.SuppressLint
import android.app.Activity
@ -7,6 +7,7 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.sukisu.ultra.R
import com.sukisu.ultra.network.RemoteToolsDownloader
import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.utils.AssetsUtil
import com.topjohnwu.superuser.Shell
@ -171,6 +172,12 @@ class HorizonKernelWorker(
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
}
try {
install()
} catch (e: Exception) {
state.updateStep("ksud update skipped: ${e.message}")
}
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
state.completeFlashing()

View file

@ -0,0 +1,726 @@
package zako.zako.zako.zakoui.screen.moreSettings
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import zako.zako.zako.zakoui.screen.moreSettings.component.ColorCircle
import zako.zako.zako.zakoui.screen.moreSettings.component.LanguageSelectionDialog
import zako.zako.zako.zakoui.screen.moreSettings.component.MoreSettingsDialogs
import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem
import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard
import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsDivider
import zako.zako.zako.zakoui.screen.moreSettings.component.SwitchSettingItem
import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState
import kotlin.math.roundToInt
@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt")
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun MoreSettingsScreen(
navigator: DestinationsNavigator
) {
// 顶部滚动行为
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val systemIsDark = isSystemInDarkTheme()
// 创建设置状态管理器
val settingsState = remember { MoreSettingsState(context, prefs, systemIsDark) }
val settingsHandlers = remember { MoreSettingsHandlers(context, prefs, settingsState) }
// 图片选择器
val pickImageLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
settingsState.selectedImageUri = it
settingsState.showImageEditor = true
}
}
// 初始化设置
LaunchedEffect(Unit) {
settingsHandlers.initializeSettings()
}
// 显示图片编辑对话框
if (settingsState.showImageEditor && settingsState.selectedImageUri != null) {
ImageEditorDialog(
imageUri = settingsState.selectedImageUri!!,
onDismiss = {
settingsState.showImageEditor = false
settingsState.selectedImageUri = null
},
onConfirm = { transformedUri ->
settingsHandlers.handleCustomBackground(transformedUri)
settingsState.showImageEditor = false
settingsState.selectedImageUri = null
}
)
}
// 各种设置对话框
MoreSettingsDialogs(
state = settingsState,
handlers = settingsHandlers
)
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.more_settings),
style = MaterialTheme.typography.titleLarge
)
},
navigationIcon = {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha),
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(top = 8.dp)
) {
// 外观设置
AppearanceSettings(
state = settingsState,
handlers = settingsHandlers,
pickImageLauncher = pickImageLauncher,
coroutineScope = coroutineScope
)
// 自定义设置
CustomizationSettings(
state = settingsState,
handlers = settingsHandlers
)
// 高级设置
KsuIsValid {
AdvancedSettings(
state = settingsState,
handlers = settingsHandlers
)
}
}
}
}
@Composable
private fun AppearanceSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers,
pickImageLauncher: ActivityResultLauncher<String>,
coroutineScope: CoroutineScope
) {
SettingsCard(title = stringResource(R.string.appearance_settings)) {
// 语言设置
LanguageSetting(state = state)
// 主题模式
SettingItem(
icon = Icons.Default.DarkMode,
title = stringResource(R.string.theme_mode),
subtitle = state.themeOptions[state.themeMode],
onClick = { state.showThemeModeDialog = true }
)
// 动态颜色开关
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SwitchSettingItem(
icon = Icons.Filled.ColorLens,
title = stringResource(R.string.dynamic_color_title),
summary = stringResource(R.string.dynamic_color_summary),
checked = state.useDynamicColor,
onChange = handlers::handleDynamicColorChange
)
}
// 主题色选择
AnimatedVisibility(
visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !state.useDynamicColor,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ThemeColorSelection(state = state)
}
SettingsDivider()
// DPI 设置
DpiSettings(state = state, handlers = handlers)
SettingsDivider()
// 自定义背景设置
CustomBackgroundSettings(
state = state,
handlers = handlers,
pickImageLauncher = pickImageLauncher,
coroutineScope = coroutineScope
)
}
}
@Composable
private fun CustomizationSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
SettingsCard(title = stringResource(R.string.custom_settings)) {
// 图标切换
SwitchSettingItem(
icon = Icons.Default.Android,
title = stringResource(R.string.icon_switch_title),
summary = stringResource(R.string.icon_switch_summary),
checked = state.useAltIcon,
onChange = handlers::handleIconChange
)
// 显示更多模块信息
SwitchSettingItem(
icon = Icons.Filled.Info,
title = stringResource(R.string.show_more_module_info),
summary = stringResource(R.string.show_more_module_info_summary),
checked = state.showMoreModuleInfo,
onChange = handlers::handleShowMoreModuleInfoChange
)
// 简洁模式开关
SwitchSettingItem(
icon = Icons.Filled.Brush,
title = stringResource(R.string.simple_mode),
summary = stringResource(R.string.simple_mode_summary),
checked = state.isSimpleMode,
onChange = handlers::handleSimpleModeChange
)
SwitchSettingItem(
icon = Icons.Filled.Brush,
title = stringResource(R.string.kernel_simple_kernel),
summary = stringResource(R.string.kernel_simple_kernel_summary),
checked = state.isKernelSimpleMode,
onChange = handlers::handleKernelSimpleModeChange
)
// 各种隐藏选项
HideOptionsSettings(state = state, handlers = handlers)
}
}
@Composable
private fun HideOptionsSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
// 隐藏内核版本号
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_kernel_kernelsu_version),
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
checked = state.isHideVersion,
onChange = handlers::handleHideVersionChange
)
// 隐藏模块数量等信息
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_other_info),
summary = stringResource(R.string.hide_other_info_summary),
checked = state.isHideOtherInfo,
onChange = handlers::handleHideOtherInfoChange
)
// SuSFS 状态信息
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_susfs_status),
summary = stringResource(R.string.hide_susfs_status_summary),
checked = state.isHideSusfsStatus,
onChange = handlers::handleHideSusfsStatusChange
)
// Zygisk 实现状态信息
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_zygisk_implement),
summary = stringResource(R.string.hide_zygisk_implement_summary),
checked = state.isHideZygiskImplement,
onChange = handlers::handleHideZygiskImplementChange
)
if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.show_kpm_info),
summary = stringResource(R.string.show_kpm_info_summary),
checked = state.isShowKpmInfo,
onChange = handlers::handleShowKpmInfoChange
)
}
// 隐藏链接信息
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_link_card),
summary = stringResource(R.string.hide_link_card_summary),
checked = state.isHideLinkCard,
onChange = handlers::handleHideLinkCardChange
)
// 隐藏标签行
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_tag_card),
summary = stringResource(R.string.hide_tag_card_summary),
checked = state.isHideTagRow,
onChange = handlers::handleHideTagRowChange
)
}
@Composable
private fun AdvancedSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
SettingsCard(title = stringResource(R.string.advanced_settings)) {
// SELinux 开关
SwitchSettingItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.selinux),
summary = if (state.selinuxEnabled)
stringResource(R.string.selinux_enabled) else
stringResource(R.string.selinux_disabled),
checked = state.selinuxEnabled,
onChange = handlers::handleSelinuxChange
)
// 动态管理器设置
if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) {
SettingItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.dynamic_manager_title),
subtitle = if (state.isDynamicSignEnabled) {
stringResource(R.string.dynamic_manager_enabled_summary, state.dynamicSignSize)
} else {
stringResource(R.string.dynamic_manager_disabled)
},
onClick = { state.showDynamicSignDialog = true }
)
}
}
}
@Composable
private fun ThemeColorSelection(state: MoreSettingsState) {
SettingItem(
icon = Icons.Default.Palette,
title = stringResource(R.string.theme_color),
subtitle = when (ThemeConfig.currentTheme) {
is ThemeColors.Green -> stringResource(R.string.color_green)
is ThemeColors.Purple -> stringResource(R.string.color_purple)
is ThemeColors.Orange -> stringResource(R.string.color_orange)
is ThemeColors.Pink -> stringResource(R.string.color_pink)
is ThemeColors.Gray -> stringResource(R.string.color_gray)
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
else -> stringResource(R.string.color_default)
},
onClick = { state.showThemeColorDialog = true },
trailingContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 8.dp)
) {
val theme = ThemeConfig.currentTheme
val isDark = isSystemInDarkTheme()
ColorCircle(
color = if (isDark) theme.primaryDark else theme.primaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
ColorCircle(
color = if (isDark) theme.secondaryDark else theme.secondaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
ColorCircle(
color = if (isDark) theme.tertiaryDark else theme.tertiaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
}
}
)
}
@Composable
private fun DpiSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
SettingItem(
icon = Icons.Default.FormatSize,
title = stringResource(R.string.app_dpi_title),
subtitle = stringResource(R.string.app_dpi_summary),
onClick = {},
trailingContent = {
Text(
text = handlers.getDpiFriendlyName(state.tempDpi),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
)
// DPI 滑动条和控制
DpiSliderControls(state = state, handlers = handlers)
}
@Composable
private fun DpiSliderControls(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
val sliderValue by animateFloatAsState(
targetValue = state.tempDpi.toFloat(),
label = "DPI Slider Animation"
)
Slider(
value = sliderValue,
onValueChange = { newValue ->
state.tempDpi = newValue.toInt()
state.isDpiCustom = !state.dpiPresets.containsValue(state.tempDpi)
},
valueRange = 160f..600f,
steps = 11,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
// DPI 预设按钮行
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
) {
state.dpiPresets.forEach { (name, dpi) ->
val isSelected = state.tempDpi == dpi
val buttonColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceVariant
Box(
modifier = Modifier
.weight(1f)
.padding(horizontal = 2.dp)
.clip(RoundedCornerShape(8.dp))
.background(buttonColor)
.clickable {
state.tempDpi = dpi
state.isDpiCustom = false
}
.padding(vertical = 8.dp, horizontal = 4.dp),
contentAlignment = Alignment.Center
) {
Text(
text = name,
style = MaterialTheme.typography.labelMedium,
color = if (isSelected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
Text(
text = if (state.isDpiCustom)
"${stringResource(R.string.dpi_size_custom)}: ${state.tempDpi}"
else
"${handlers.getDpiFriendlyName(state.tempDpi)}: ${state.tempDpi}",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp)
)
Button(
onClick = { state.showDpiConfirmDialog = true },
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
enabled = state.tempDpi != state.currentDpi
) {
Icon(
Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.dpi_apply_settings))
}
}
}
@Composable
private fun CustomBackgroundSettings(
state: MoreSettingsState,
handlers: MoreSettingsHandlers,
pickImageLauncher: ActivityResultLauncher<String>,
coroutineScope: CoroutineScope
) {
// 自定义背景开关
SwitchSettingItem(
icon = Icons.Filled.Wallpaper,
title = stringResource(id = R.string.settings_custom_background),
summary = stringResource(id = R.string.settings_custom_background_summary),
checked = state.isCustomBackgroundEnabled,
onChange = { isChecked ->
if (isChecked) {
pickImageLauncher.launch("image/*")
} else {
handlers.handleRemoveCustomBackground()
}
}
)
// 透明度和亮度调节
AnimatedVisibility(
visible = ThemeConfig.customBackgroundUri != null,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
BackgroundAdjustmentControls(
state = state,
handlers = handlers,
coroutineScope = coroutineScope
)
}
}
@Composable
private fun BackgroundAdjustmentControls(
state: MoreSettingsState,
handlers: MoreSettingsHandlers,
coroutineScope: CoroutineScope
) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
// 透明度滑动条
AlphaSlider(state = state, handlers = handlers, coroutineScope = coroutineScope)
// 亮度调节滑动条
DimSlider(state = state, handlers = handlers, coroutineScope = coroutineScope)
}
}
@Composable
private fun AlphaSlider(
state: MoreSettingsState,
handlers: MoreSettingsHandlers,
coroutineScope: CoroutineScope
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(
Icons.Filled.Opacity,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.settings_card_alpha),
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${(state.cardAlpha * 100).roundToInt()}%",
style = MaterialTheme.typography.labelMedium,
)
}
val alphaSliderValue by animateFloatAsState(
targetValue = state.cardAlpha,
label = "Alpha Slider Animation"
)
Slider(
value = alphaSliderValue,
onValueChange = { newValue ->
handlers.handleCardAlphaChange(newValue)
},
onValueChangeFinished = {
coroutineScope.launch(Dispatchers.IO) {
saveCardConfig(handlers.context)
}
},
valueRange = 0f..1f,
steps = 20,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
@Composable
private fun DimSlider(
state: MoreSettingsState,
handlers: MoreSettingsHandlers,
coroutineScope: CoroutineScope
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp)
) {
Icon(
Icons.Filled.LightMode,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.settings_card_dim),
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${(state.cardDim * 100).roundToInt()}%",
style = MaterialTheme.typography.labelMedium,
)
}
val dimSliderValue by animateFloatAsState(
targetValue = state.cardDim,
label = "Dim Slider Animation"
)
Slider(
value = dimSliderValue,
onValueChange = { newValue ->
handlers.handleCardDimChange(newValue)
},
onValueChangeFinished = {
coroutineScope.launch(Dispatchers.IO) {
saveCardConfig(handlers.context)
}
},
valueRange = 0f..1f,
steps = 20,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
fun saveCardConfig(context: Context) {
CardConfig.save(context)
}
@Composable
private fun LanguageSetting(state: MoreSettingsState) {
val context = LocalContext.current
val language = stringResource(id = R.string.settings_language)
// Compute display name based on current app locale
val currentLanguageDisplay = remember(state.currentAppLocale) {
val locale = state.currentAppLocale
if (locale != null) {
locale.getDisplayName(locale)
} else {
context.getString(R.string.language_system_default)
}
}
SettingItem(
icon = Icons.Filled.Translate,
title = language,
subtitle = currentLanguageDisplay,
onClick = { state.showLanguageDialog = true }
)
// Language Selection Dialog
if (state.showLanguageDialog) {
LanguageSelectionDialog(
onLanguageSelected = { newLocale ->
// Update local state immediately
state.currentAppLocale = LocaleHelper.getCurrentAppLocale(context)
// Apply locale change immediately for Android < 13
LocaleHelper.restartActivity(context)
},
onDismiss = { state.showLanguageDialog = false }
)
}
}

View file

@ -0,0 +1,436 @@
package zako.zako.zako.zakoui.screen.moreSettings
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.util.*
import com.topjohnwu.superuser.Shell
import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState
import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon
/**
* 更多设置处理器
*/
class MoreSettingsHandlers(
val context: Context,
private val prefs: SharedPreferences,
private val state: MoreSettingsState
) {
/**
* 初始化设置
*/
fun initializeSettings() {
// 加载设置
CardConfig.load(context)
state.cardAlpha = CardConfig.cardAlpha
state.cardDim = CardConfig.cardDim
state.isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null
// 设置主题模式
state.themeMode = when (ThemeConfig.forceDarkMode) {
true -> 2
false -> 1
null -> 0
}
// 确保卡片样式跟随主题模式
when (state.themeMode) {
2 -> { // 深色
CardConfig.isUserDarkModeEnabled = true
CardConfig.isUserLightModeEnabled = false
}
1 -> { // 浅色
CardConfig.isUserDarkModeEnabled = false
CardConfig.isUserLightModeEnabled = true
}
0 -> { // 跟随系统
CardConfig.isUserDarkModeEnabled = false
CardConfig.isUserLightModeEnabled = false
}
}
// 如果启用了系统跟随且系统是深色模式,应用深色模式默认值
if (state.themeMode == 0 && state.systemIsDark) {
CardConfig.setThemeDefaults(true)
}
state.currentDpi = prefs.getInt("app_dpi", state.systemDpi)
state.tempDpi = state.currentDpi
CardConfig.save(context)
// 初始化 SELinux 状态
state.selinuxEnabled = Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing"
// 初始化动态管理器配置
state.dynamicSignConfig = Natives.getDynamicManager()
state.dynamicSignConfig?.let { config ->
if (config.isValid()) {
state.isDynamicSignEnabled = true
state.dynamicSignSize = config.size.toString()
state.dynamicSignHash = config.hash
}
}
}
/**
* 处理主题模式变更
*/
fun handleThemeModeChange(index: Int) {
state.themeMode = index
val newThemeMode = when (index) {
0 -> null // 跟随系统
1 -> false // 浅色
2 -> true // 深色
else -> null
}
context.saveThemeMode(newThemeMode)
ThemeConfig.updateTheme(darkMode = newThemeMode)
when (index) {
2 -> { // 深色
ThemeConfig.updateTheme(darkMode = true)
CardConfig.updateThemePreference(darkMode = true, lightMode = false)
CardConfig.setThemeDefaults(true)
CardConfig.save(context)
}
1 -> { // 浅色
ThemeConfig.updateTheme(darkMode = false)
CardConfig.updateThemePreference(darkMode = false, lightMode = true)
CardConfig.setThemeDefaults(false)
CardConfig.save(context)
}
0 -> { // 跟随系统
ThemeConfig.updateTheme(darkMode = null)
CardConfig.updateThemePreference(darkMode = null, lightMode = null)
val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
CardConfig.setThemeDefaults(isNightModeActive)
CardConfig.save(context)
}
}
}
/**
* 处理主题色变更
*/
fun handleThemeColorChange(theme: ThemeColors) {
context.saveThemeColors(when (theme) {
ThemeColors.Green -> "green"
ThemeColors.Purple -> "purple"
ThemeColors.Orange -> "orange"
ThemeColors.Pink -> "pink"
ThemeColors.Gray -> "gray"
ThemeColors.Yellow -> "yellow"
else -> "default"
})
ThemeConfig.updateTheme(theme = theme)
}
/**
* 处理动态颜色变更
*/
fun handleDynamicColorChange(enabled: Boolean) {
state.useDynamicColor = enabled
context.saveDynamicColorState(enabled)
ThemeConfig.updateTheme(dynamicColor = enabled)
}
/**
* 获取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)
}
}
/**
* 应用 DPI 设置
*/
fun handleDpiApply() {
if (state.tempDpi != state.currentDpi) {
prefs.edit {
putInt("app_dpi", state.tempDpi)
}
state.currentDpi = state.tempDpi
Toast.makeText(
context,
context.getString(R.string.dpi_applied_success, state.tempDpi),
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)
state.showDpiConfirmDialog = false
}
}
/**
* 处理自定义背景
*/
fun handleCustomBackground(transformedUri: Uri) {
context.saveAndApplyCustomBackground(transformedUri)
state.isCustomBackgroundEnabled = true
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
saveCardConfig(context)
Toast.makeText(
context,
context.getString(R.string.background_set_success),
Toast.LENGTH_SHORT
).show()
}
/**
* 处理移除自定义背景
*/
fun handleRemoveCustomBackground() {
context.saveCustomBackground(null)
state.isCustomBackgroundEnabled = false
CardConfig.cardAlpha = 1f
CardConfig.cardDim = 0f
CardConfig.isCustomAlphaSet = false
CardConfig.isCustomDimSet = false
CardConfig.isCustomBackgroundEnabled = false
saveCardConfig(context)
ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
Toast.makeText(
context,
context.getString(R.string.background_removed),
Toast.LENGTH_SHORT
).show()
}
/**
* 处理卡片透明度变更
*/
fun handleCardAlphaChange(newValue: Float) {
state.cardAlpha = newValue
CardConfig.cardAlpha = newValue
CardConfig.isCustomAlphaSet = true
prefs.edit {
putBoolean("is_custom_alpha_set", true)
putFloat("card_alpha", newValue)
}
}
/**
* 处理卡片亮度变更
*/
fun handleCardDimChange(newValue: Float) {
state.cardDim = newValue
CardConfig.cardDim = newValue
CardConfig.isCustomDimSet = true
prefs.edit {
putBoolean("is_custom_dim_set", true)
putFloat("card_dim", newValue)
}
}
/**
* 处理图标变更
*/
fun handleIconChange(newValue: Boolean) {
prefs.edit { putBoolean("use_alt_icon", newValue) }
state.useAltIcon = newValue
toggleLauncherIcon(context, newValue)
Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show()
}
/**
* 处理简洁模式变更
*/
fun handleSimpleModeChange(newValue: Boolean) {
prefs.edit { putBoolean("is_simple_mode", newValue) }
state.isSimpleMode = newValue
}
/**
* 处理内核简洁模式变更
*/
fun handleKernelSimpleModeChange(newValue: Boolean) {
prefs.edit { putBoolean("is_kernel_simple_mode", newValue) }
state.isKernelSimpleMode = newValue
}
/**
* 处理隐藏版本变更
*/
fun handleHideVersionChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_version", newValue) }
state.isHideVersion = newValue
}
/**
* 处理隐藏其他信息变更
*/
fun handleHideOtherInfoChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_other_info", newValue) }
state.isHideOtherInfo = newValue
}
/**
* 处理显示KPM信息变更
*/
fun handleShowKpmInfoChange(newValue: Boolean) {
prefs.edit { putBoolean("show_kpm_info", newValue) }
state.isShowKpmInfo = newValue
}
/**
* 处理隐藏SuSFS状态变更
*/
fun handleHideSusfsStatusChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
state.isHideSusfsStatus = newValue
}
/**
* 处理隐藏Zygisk实现变更
*/
fun handleHideZygiskImplementChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) }
state.isHideZygiskImplement = newValue
}
/**
* 处理隐藏链接卡片变更
*/
fun handleHideLinkCardChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_link_card", newValue) }
state.isHideLinkCard = newValue
}
/**
* 处理隐藏标签行变更
*/
fun handleHideTagRowChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_tag_row", newValue) }
state.isHideTagRow = newValue
}
/**
* 处理显示更多模块信息变更
*/
fun handleShowMoreModuleInfoChange(newValue: Boolean) {
prefs.edit { putBoolean("show_more_module_info", newValue) }
state.showMoreModuleInfo = newValue
}
/**
* 处理SELinux变更
*/
fun handleSelinuxChange(enabled: Boolean) {
val command = if (enabled) "setenforce 1" else "setenforce 0"
Shell.getShell().newJob().add(command).exec().let { result ->
if (result.isSuccess) {
state.selinuxEnabled = enabled
val message = if (enabled)
context.getString(R.string.selinux_enabled_toast)
else
context.getString(R.string.selinux_disabled_toast)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(
context,
context.getString(R.string.selinux_change_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
/**
* 处理动态管理器配置
*/
fun handleDynamicManagerConfig(enabled: Boolean, size: String, hash: String) {
if (enabled) {
val parsedSize = parseDynamicSignSize(size)
if (parsedSize != null && parsedSize > 0 && hash.length == 64) {
val success = Natives.setDynamicManager(parsedSize, hash)
if (success) {
state.dynamicSignConfig = Natives.DynamicManagerConfig(parsedSize, hash)
state.isDynamicSignEnabled = true
state.dynamicSignSize = size
state.dynamicSignHash = hash
Toast.makeText(
context,
context.getString(R.string.dynamic_manager_set_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.dynamic_manager_set_failed),
Toast.LENGTH_SHORT
).show()
}
} else {
Toast.makeText(
context,
context.getString(R.string.invalid_sign_config),
Toast.LENGTH_SHORT
).show()
}
} else {
val success = Natives.clearDynamicManager()
if (success) {
state.dynamicSignConfig = null
state.isDynamicSignEnabled = false
state.dynamicSignSize = ""
state.dynamicSignHash = ""
Toast.makeText(
context,
context.getString(R.string.dynamic_manager_disabled_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.dynamic_manager_clear_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
/**
* 解析动态签名大小
*/
private fun parseDynamicSignSize(input: String): Int? {
return try {
when {
input.startsWith("0x", true) -> input.substring(2).toInt(16)
else -> input.toInt()
}
} catch (_: NumberFormatException) {
null
}
}
}

View file

@ -0,0 +1,201 @@
package zako.zako.zako.zakoui.screen.moreSettings.component
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.NavigateNext
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sukisu.ultra.ui.theme.*
private val SETTINGS_GROUP_SPACING = 16.dp
@Composable
fun SettingsCard(
title: String,
icon: ImageVector? = null,
content: @Composable () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = SETTINGS_GROUP_SPACING),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = getCardElevation(),
shape = MaterialTheme.shapes.medium
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.padding(horizontal = 16.dp)
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
}
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
)
}
content()
}
}
}
@Composable
fun SettingItem(
icon: ImageVector,
title: String,
subtitle: String? = null,
onClick: () -> Unit,
iconTint: Color = MaterialTheme.colorScheme.primary,
trailingContent: @Composable (() -> Unit)? = {
Icon(
Icons.AutoMirrored.Filled.NavigateNext,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 5.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
maxLines = Int.MAX_VALUE,
overflow = TextOverflow.Visible
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = Int.MAX_VALUE,
overflow = TextOverflow.Visible
)
}
}
trailingContent?.invoke()
}
}
@Composable
fun SwitchSettingItem(
icon: ImageVector,
title: String,
summary: String? = null,
checked: Boolean,
onChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onChange(!checked) }
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
lineHeight = 20.sp,
)
if (summary != null) {
Spacer(modifier = Modifier.height(2.dp))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 16.sp,
)
}
}
Switch(
checked = checked,
onCheckedChange = onChange
)
}
}
@Composable
fun SettingsDivider() {
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp)
)
}
@Composable
fun ColorCircle(
color: Color,
isSelected: Boolean,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.size(20.dp)
.clip(CircleShape)
.background(color)
.then(
if (isSelected) {
Modifier.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape
)
} else {
Modifier
}
)
)
}

View file

@ -0,0 +1,476 @@
package zako.zako.zako.zakoui.screen.moreSettings.component
import android.content.Context
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.*
import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers
import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState
@Composable
fun MoreSettingsDialogs(
state: MoreSettingsState,
handlers: MoreSettingsHandlers
) {
// 主题模式选择对话框
if (state.showThemeModeDialog) {
SingleChoiceDialog(
title = stringResource(R.string.theme_mode),
options = state.themeOptions,
selectedIndex = state.themeMode,
onOptionSelected = { index ->
handlers.handleThemeModeChange(index)
},
onDismiss = { state.showThemeModeDialog = false }
)
}
// DPI 设置确认对话框
if (state.showDpiConfirmDialog) {
ConfirmDialog(
title = stringResource(R.string.dpi_confirm_title),
message = stringResource(R.string.dpi_confirm_message, state.currentDpi, state.tempDpi),
summaryText = stringResource(R.string.dpi_confirm_summary),
confirmText = stringResource(R.string.confirm),
dismissText = stringResource(R.string.cancel),
onConfirm = { handlers.handleDpiApply() },
onDismiss = {
state.showDpiConfirmDialog = false
state.tempDpi = state.currentDpi
}
)
}
// 主题色选择对话框
if (state.showThemeColorDialog) {
ThemeColorDialog(
onColorSelected = { theme ->
handlers.handleThemeColorChange(theme)
state.showThemeColorDialog = false
},
onDismiss = { state.showThemeColorDialog = false }
)
}
// 动态管理器配置对话框
if (state.showDynamicSignDialog) {
DynamicManagerDialog(
state = state,
onConfirm = { enabled, size, hash ->
handlers.handleDynamicManagerConfig(enabled, size, hash)
state.showDynamicSignDialog = false
},
onDismiss = { state.showDynamicSignDialog = false }
)
}
}
@Composable
fun SingleChoiceDialog(
title: String,
options: List<String>,
selectedIndex: Int,
onOptionSelected: (Int) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
options.forEachIndexed { index, option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onOptionSelected(index)
onDismiss()
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedIndex == index,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(option)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}
@Composable
fun ConfirmDialog(
title: String,
message: String,
summaryText: String? = null,
confirmText: String = stringResource(R.string.confirm),
dismissText: String = stringResource(R.string.cancel),
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = {
Column {
Text(message)
if (summaryText != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
summaryText,
style = MaterialTheme.typography.bodySmall
)
}
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(confirmText)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(dismissText)
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LanguageSelectionDialog(
onLanguageSelected: (String) -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
// Check if should use system language settings
if (LocaleHelper.useSystemLanguageSettings) {
// Android 13+ - Jump to system settings
LocaleHelper.launchSystemLanguageSettings(context)
onDismiss()
} else {
// Android < 13 - Show app language selector
// Dynamically detect supported locales from resources
val supportedLocales = remember {
val locales = mutableListOf<java.util.Locale>()
// Add system default first
locales.add(java.util.Locale.ROOT) // This will represent "System Default"
// Dynamically detect available locales by checking resource directories
val resourceDirs = listOf(
"ar", "bg", "de", "fa", "fr", "hu", "in", "it",
"ja", "ko", "pl", "pt-rBR", "ru", "th", "tr",
"uk", "vi", "zh-rCN", "zh-rTW"
)
resourceDirs.forEach { dir ->
try {
val locale = when {
dir.contains("-r") -> {
val parts = dir.split("-r")
java.util.Locale.Builder()
.setLanguage(parts[0])
.setRegion(parts[1])
.build()
}
else -> java.util.Locale.Builder()
.setLanguage(dir)
.build()
}
// Test if this locale has translated resources
val config = android.content.res.Configuration()
config.setLocale(locale)
val localizedContext = context.createConfigurationContext(config)
// Try to get a translated string to verify the locale is supported
val testString = localizedContext.getString(R.string.settings_language)
val defaultString = context.getString(R.string.settings_language)
// If the string is different or it's English, it's supported
if (testString != defaultString || locale.language == "en") {
locales.add(locale)
}
} catch (_: Exception) {
// Skip unsupported locales
}
}
// Sort by display name
val sortedLocales = locales.drop(1).sortedBy { it.getDisplayName(it) }
mutableListOf<java.util.Locale>().apply {
add(locales.first()) // System default first
addAll(sortedLocales)
}
}
val allOptions = supportedLocales.map { locale ->
val tag = if (locale == java.util.Locale.ROOT) {
"system"
} else if (locale.country.isEmpty()) {
locale.language
} else {
"${locale.language}_${locale.country}"
}
val displayName = if (locale == java.util.Locale.ROOT) {
context.getString(R.string.language_system_default)
} else {
locale.getDisplayName(locale)
}
tag to displayName
}
val currentLocale = prefs.getString("app_locale", "system") ?: "system"
val options = allOptions.map { (tag, displayName) ->
ListOption(
titleText = displayName,
selected = currentLocale == tag
)
}
var selectedIndex by remember {
mutableIntStateOf(allOptions.indexOfFirst { (tag, _) -> currentLocale == tag })
}
ListDialog(
state = rememberUseCaseState(
visible = true,
onFinishedRequest = {
if (selectedIndex >= 0 && selectedIndex < allOptions.size) {
val newLocale = allOptions[selectedIndex].first
prefs.edit { putString("app_locale", newLocale) }
onLanguageSelected(newLocale)
}
onDismiss()
},
onCloseRequest = {
onDismiss()
}
),
header = Header.Default(
title = stringResource(R.string.settings_language),
),
selection = ListSelection.Single(
showRadioButtons = true,
options = options
) { index, _ ->
selectedIndex = index
}
)
}
}
@Composable
fun ThemeColorDialog(
onColorSelected: (ThemeColors) -> Unit,
onDismiss: () -> Unit
) {
val themeColorOptions = listOf(
stringResource(R.string.color_default) to ThemeColors.Default,
stringResource(R.string.color_green) to ThemeColors.Green,
stringResource(R.string.color_purple) to ThemeColors.Purple,
stringResource(R.string.color_orange) to ThemeColors.Orange,
stringResource(R.string.color_pink) to ThemeColors.Pink,
stringResource(R.string.color_gray) to ThemeColors.Gray,
stringResource(R.string.color_yellow) to ThemeColors.Yellow
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.choose_theme_color)) },
text = {
Column {
themeColorOptions.forEach { (name, theme) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onColorSelected(theme) }
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val isDark = isSystemInDarkTheme()
Box(
modifier = Modifier.padding(end = 12.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
ColorCircle(
color = if (isDark) theme.primaryDark else theme.primaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
ColorCircle(
color = if (isDark) theme.secondaryDark else theme.secondaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
ColorCircle(
color = if (isDark) theme.tertiaryDark else theme.tertiaryLight,
isSelected = false,
modifier = Modifier.padding(horizontal = 2.dp)
)
}
}
Text(name)
Spacer(modifier = Modifier.weight(1f))
// 当前选中的主题显示选中标记
if (ThemeConfig.currentTheme::class == theme::class) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
}
}
}
},
confirmButton = {
Button(
onClick = onDismiss
) {
Text(stringResource(R.string.cancel))
}
}
)
}
@Composable
fun DynamicManagerDialog(
state: MoreSettingsState,
onConfirm: (Boolean, String, String) -> Unit,
onDismiss: () -> Unit
) {
var localEnabled by remember { mutableStateOf(state.isDynamicSignEnabled) }
var localSize by remember { mutableStateOf(state.dynamicSignSize) }
var localHash by remember { mutableStateOf(state.dynamicSignHash) }
fun parseDynamicSignSize(input: String): Int? {
return try {
when {
input.startsWith("0x", true) -> input.substring(2).toInt(16)
else -> input.toInt()
}
} catch (_: NumberFormatException) {
null
}
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.dynamic_manager_title)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
// 启用开关
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { localEnabled = !localEnabled }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Switch(
checked = localEnabled,
onCheckedChange = { localEnabled = it }
)
Spacer(modifier = Modifier.width(12.dp))
Text(stringResource(R.string.enable_dynamic_manager))
}
Spacer(modifier = Modifier.height(16.dp))
// 签名大小输入
OutlinedTextField(
value = localSize,
onValueChange = { input ->
val isValid = when {
input.isEmpty() -> true
input.matches(Regex("^\\d+$")) -> true
input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true
else -> false
}
if (isValid) {
localSize = input
}
},
label = { Text(stringResource(R.string.signature_size)) },
enabled = localEnabled,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text
)
)
Spacer(modifier = Modifier.height(12.dp))
// 签名哈希输入
OutlinedTextField(
value = localHash,
onValueChange = { hash ->
if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) {
localHash = hash
}
},
label = { Text(stringResource(R.string.signature_hash)) },
enabled = localEnabled,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
supportingText = {
Text(stringResource(R.string.hash_must_be_64_chars))
},
isError = localEnabled && localHash.isNotEmpty() && localHash.length != 64
)
}
},
confirmButton = {
Button(
onClick = { onConfirm(localEnabled, localSize, localHash) },
enabled = if (localEnabled) {
parseDynamicSignSize(localSize)?.let { it > 0 } == true &&
localHash.length == 64
} else true
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}

View file

@ -0,0 +1,104 @@
package zako.zako.zako.zakoui.screen.moreSettings.state
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeConfig
/**
* 更多设置状态管理
*/
@Stable
class MoreSettingsState(
val context: Context,
val prefs: SharedPreferences,
val systemIsDark: Boolean
) {
// 主题模式选择
var themeMode by mutableIntStateOf(
when (ThemeConfig.forceDarkMode) {
true -> 2 // 深色
false -> 1 // 浅色
null -> 0 // 跟随系统
}
)
// 动态颜色开关状态
var useDynamicColor by mutableStateOf(ThemeConfig.useDynamicColor)
// 语言设置
var showLanguageDialog by mutableStateOf(false)
var currentAppLocale by mutableStateOf(LocaleHelper.getCurrentAppLocale(context))
// 对话框显示状态
var showThemeModeDialog by mutableStateOf(false)
var showThemeColorDialog by mutableStateOf(false)
var showDpiConfirmDialog by mutableStateOf(false)
var showImageEditor by mutableStateOf(false)
// 动态管理器配置状态
var dynamicSignConfig by mutableStateOf<Natives.DynamicManagerConfig?>(null)
var isDynamicSignEnabled by mutableStateOf(false)
var dynamicSignSize by mutableStateOf("")
var dynamicSignHash by mutableStateOf("")
var showDynamicSignDialog by mutableStateOf(false)
// 各种设置开关状态
var isSimpleMode by mutableStateOf(prefs.getBoolean("is_simple_mode", false))
var isHideVersion by mutableStateOf(prefs.getBoolean("is_hide_version", false))
var isHideOtherInfo by mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
var isShowKpmInfo by mutableStateOf(prefs.getBoolean("show_kpm_info", false))
var isHideZygiskImplement by mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false))
var isHideSusfsStatus by mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
var isHideLinkCard by mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
var isHideTagRow by mutableStateOf(prefs.getBoolean("is_hide_tag_row", false))
var isKernelSimpleMode by mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false))
var showMoreModuleInfo by mutableStateOf(prefs.getBoolean("show_more_module_info", false))
var useAltIcon by mutableStateOf(prefs.getBoolean("use_alt_icon", false))
// SELinux状态
var selinuxEnabled by mutableStateOf(false)
// SuSFS 状态
var isSusFSEnabled by mutableStateOf(true)
// 卡片配置状态
var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha)
var cardDim by mutableFloatStateOf(CardConfig.cardDim)
var isCustomBackgroundEnabled by mutableStateOf(ThemeConfig.customBackgroundUri != null)
// 图片选择状态
var selectedImageUri by mutableStateOf<Uri?>(null)
// DPI 设置
val systemDpi = context.resources.displayMetrics.densityDpi
var currentDpi by mutableIntStateOf(prefs.getInt("app_dpi", systemDpi))
var tempDpi by mutableIntStateOf(currentDpi)
var isDpiCustom by mutableStateOf(true)
// 主题模式选项
val themeOptions = listOf(
context.getString(R.string.theme_follow_system),
context.getString(R.string.theme_light),
context.getString(R.string.theme_dark)
)
// 预设 DPI 选项
val dpiPresets = mapOf(
context.getString(R.string.dpi_size_small) to 240,
context.getString(R.string.dpi_size_medium) to 320,
context.getString(R.string.dpi_size_large) to 420,
context.getString(R.string.dpi_size_extra_large) to 560
)
}

View file

@ -0,0 +1,154 @@
package zako.zako.zako.zakoui.screen.moreSettings.util
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.provider.Settings
import java.util.*
object LocaleHelper {
/**
* Check if should use system language settings (Android 13+)
*/
val useSystemLanguageSettings: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
/**
* Launch system app locale settings (Android 13+)
*/
fun launchSystemLanguageSettings(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
} catch (_: Exception) {
// Fallback to app language settings if system settings not available
}
}
}
/**
* Apply saved language setting to context (for Android < 13)
*/
fun applyLanguage(context: Context): Context {
// On Android 13+, language is handled by system
if (useSystemLanguageSettings) {
return context
}
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val localeTag = prefs.getString("app_locale", "system") ?: "system"
return if (localeTag == "system") {
context
} else {
val locale = parseLocaleTag(localeTag)
setLocale(context, locale)
}
}
/**
* Set locale for context (Android < 13)
*/
@SuppressLint("ObsoleteSdkInt")
private fun setLocale(context: Context, locale: Locale): Context {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
updateResources(context, locale)
} else {
updateResourcesLegacy(context, locale)
}
}
@SuppressLint("UseRequiresApi", "ObsoleteSdkInt")
@TargetApi(Build.VERSION_CODES.N)
private fun updateResources(context: Context, locale: Locale): Context {
val configuration = Configuration()
configuration.setLocale(locale)
configuration.setLayoutDirection(locale)
return context.createConfigurationContext(configuration)
}
@Suppress("DEPRECATION")
@SuppressWarnings("deprecation")
private fun updateResourcesLegacy(context: Context, locale: Locale): Context {
Locale.setDefault(locale)
val resources = context.resources
val configuration = resources.configuration
configuration.locale = locale
configuration.setLayoutDirection(locale)
resources.updateConfiguration(configuration, resources.displayMetrics)
return context
}
/**
* Parse locale tag to Locale object
*/
private fun parseLocaleTag(tag: String): Locale {
return try {
if (tag.contains("_")) {
val parts = tag.split("_")
Locale.Builder()
.setLanguage(parts[0])
.setRegion(parts.getOrNull(1) ?: "")
.build()
} else {
Locale.Builder()
.setLanguage(tag)
.build()
}
} catch (_: Exception) {
Locale.getDefault()
}
}
/**
* Restart activity to apply language change (Android < 13)
*/
fun restartActivity(context: Context) {
if (context is Activity && !useSystemLanguageSettings) {
context.recreate()
}
}
/**
* Get current app locale
*/
@SuppressLint("ObsoleteSdkInt")
fun getCurrentAppLocale(context: Context): Locale? {
return if (useSystemLanguageSettings) {
// Android 13+ - get from system app locale settings
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
val localeManager = context.getSystemService(Context.LOCALE_SERVICE) as? android.app.LocaleManager
val locales = localeManager?.applicationLocales
if (locales != null && !locales.isEmpty) {
locales.get(0)
} else {
null // System default
}
} catch (_: Exception) {
null // System default
}
} else {
null // System default
}
} else {
// Android < 13 - get from SharedPreferences
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val localeTag = prefs.getString("app_locale", "system") ?: "system"
if (localeTag == "system") {
null // System default
} else {
parseLocaleTag(localeTag)
}
}
}
}

View file

@ -0,0 +1,27 @@
package zako.zako.zako.zakoui.screen.moreSettings.util
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import com.sukisu.ultra.ui.MainActivity
/**
* 刷新启动器图标
*/
fun toggleLauncherIcon(context: Context, useAlt: Boolean) {
val pm = context.packageManager
val main = ComponentName(context, MainActivity::class.java.name)
val alias = ComponentName(context, "${MainActivity::class.java.name}Alias")
pm.setComponentEnabledSetting(
if (useAlt) alias else main,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
if (useAlt) main else alias,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}

View file

@ -1,5 +1,8 @@
libzakozako.so
libzakozakozako.so
libkpmmgr.so
libzako.so
libandroidx.graphics.path.so
libksud.so
libkernelsu.so
libsusfsd.so
libuid_scanner.so
libzakosign.so
libandroidx.graphics.path.so
libmmrl-file-manager.so
libmmrl-kernelsu.so

Binary file not shown.

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">إصدار KernelSU الحالي %s منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %s أو أعلى!</string>
<string name="settings_umount_modules_default">الغاء تحميل الإضافات بشكل افتراضي</string>
<string name="settings_umount_modules_default_summary">القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف.</string>
<string name="settings_susfs_toggle">تعطيل روابط kprobe</string>
<string name="profile_umount_modules_summary">سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق.</string>
<string name="profile_selinux_domain">المجال</string>
<string name="profile_selinux_rules">القواعد</string>
@ -235,7 +234,6 @@
<string name="invalid_file_type">نوع الملف غير صحيح! الرجاء تحديد ملف .kpm.</string>
<string name="confirm_uninstall_title_with_filename">إلغاء التثبيت</string>
<string name="confirm_uninstall_content">سيتم إلغاء تثبيت KPM التالية: %s</string>
<string name="settings_susfs_toggle_summary">تعطيل روابط kprobe التي أنشأتها KernelSU، باستخدام الروابط الواردة بدلاً من ذلك، والتي تشبه طريقة الربط غير GKI غير GKI.</string>
<string name="image_editor_hint">استخدم إصبعين لتكبير الصورة، وأصبع واحد لسحبها لضبط الموضع</string>
<string name="reprovision">إعادة</string>
<!-- Kernel Flash Progress Related -->
@ -309,9 +307,8 @@
<string name="dpi_confirm_summary">يحتاج التطبيق إلى إعادة تشغيل لتطبيق الإعدادات الجديدة لإدارة شؤون الإعلام، ولا يؤثر على شريط حالة النظام أو التطبيقات الأخرى</string>
<string name="dpi_applied_success">تم تعيين DPI إلى %1$d، فعلي بعد إعادة تشغيل التطبيق</string>
<!-- Language settings related strings -->
<string name="language_setting">لغة التطبيق</string>
<string name="language_follow_system">اتبع النظام</string>
<string name="language_changed">تم تغيير اللغة، إعادة التشغيل لتطبيق التغييرات</string>
<string name="settings_language">لغة التطبيق</string>
<string name="language_system_default">اتبع النظام</string>
<string name="settings_card_dim">تعديل ظلام البطاقة</string>
<!-- Flash related -->
<string name="error_code">رمز الخطأ</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Defolt olaraq modulları umount et</string>
<string name="settings_umount_modules_default_summary">Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək.</string>
<string name="profile_selinux_domain">Domen</string>
<string name="profile_selinux_rules">Qaydalar</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Umount module po zadanom</string>
<string name="settings_umount_modules_default_summary">Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju.</string>
<string name="profile_selinux_domain">Domena</string>
<string name="profile_selinux_rules">Pravila</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Afmontere moduler som standard</string>
<string name="settings_umount_modules_default_summary">Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation.</string>
<string name="profile_selinux_domain">Domæne</string>
<string name="profile_selinux_rules">Regler</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">Die aktuelle KernelSU-Version %s ist zu alt für diese Manager-Version. Bitte auf Version %s oder höher aktualisieren!</string>
<string name="settings_umount_modules_default">Module standardmäßig aushängen</string>
<string name="settings_umount_modules_default_summary">Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist.</string>
<string name="settings_susfs_toggle">Kprobe-Hooks deaktivieren</string>
<string name="profile_umount_modules_summary">Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen.</string>
<string name="profile_selinux_domain">Domäne</string>
<string name="profile_selinux_rules">Regeln</string>
@ -235,7 +234,6 @@
<string name="invalid_file_type">Falscher Dateityp! Bitte wählen Sie eine .kpm Datei.</string>
<string name="confirm_uninstall_title_with_filename">Deinstallieren</string>
<string name="confirm_uninstall_content">Folgende KPM wird deinstalliert: %s</string>
<string name="settings_susfs_toggle_summary">Deaktiviere kprobe Hooks die von KernelSU erstellt wurden und stattdessen inline Hooks verwenden, was der Nicht-GKI-Kernel-Hooking Methode ähnlich ist.</string>
<string name="image_editor_hint">Verwende zwei Finger um das Bild zu vergrößern und einen Finger um die Position anzupassen</string>
<string name="reprovision">Rückzahlung</string>
<!-- Kernel Flash Progress Related -->
@ -309,9 +307,8 @@
<string name="dpi_confirm_summary">Die Anwendung muss neu gestartet werden, um die neuen DPI-Einstellungen zu übernehmen, hat keine Auswirkungen auf die System-Statusleiste oder andere Anwendungen</string>
<string name="dpi_applied_success">DPI wurde auf %1$dgesetzt, wirksam nach dem Neustart der Anwendung</string>
<!-- Language settings related strings -->
<string name="language_setting">App Sprache</string>
<string name="language_follow_system">Folge Systemeinstellung</string>
<string name="language_changed">Sprache geändert, Neustart um Änderungen zu übernehmen</string>
<string name="settings_language">App Sprache</string>
<string name="language_system_default">Folge Systemeinstellung</string>
<string name="settings_card_dim">Kartenfinsternis Anpassung</string>
<!-- Flash related -->
<string name="error_code">fehlercode</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">La versión %s actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %s o superior!</string>
<string name="settings_umount_modules_default">Desmontar módulos por defecto</string>
<string name="settings_umount_modules_default_summary">El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido.</string>
<string name="settings_susfs_toggle">Desactivar kprobe hooks</string>
<string name="profile_umount_modules_summary">Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación.</string>
<string name="profile_selinux_domain">Dominio</string>
<string name="profile_selinux_rules">Reglas</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">¡Tipo de archivo incorrecto! Por favor seleccione el archivo .kpm.</string>
<string name="confirm_uninstall_title_with_filename">Desinstalar</string>
<string name="confirm_uninstall_content">El siguiente KPM será desinstalado: %s</string>
<string name="settings_susfs_toggle_summary">Deshabilita los ganchos kprobe creados por KernelSU, usando ganchos en línea en su lugar, que es similar al método de enganche del núcleo no GKI.</string>
<string name="image_editor_hint">Usa dos dedos para acercar la imagen, y un dedo para arrastrarla para ajustar la posición</string>
<string name="reprovision">Reaprovisionamiento</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<string name="dpi_confirm_summary">La aplicación necesita reiniciarse para aplicar la nueva configuración DPI, no afecta a la barra de estado del sistema u otras aplicaciones</string>
<string name="dpi_applied_success">DPI ha sido establecido a %1$d, efectivo después de reiniciar la aplicación</string>
<!-- Language settings related strings -->
<string name="language_setting">Idioma de la aplicación</string>
<string name="language_follow_system">Seguir sistema</string>
<string name="language_changed">Idioma cambiado, reiniciando para aplicar cambios</string>
<string name="settings_language">Idioma de la aplicación</string>
<string name="language_system_default">Seguir sistema</string>
<string name="settings_card_dim">Ajuste de oscuridad de tarjeta</string>
<!-- Flash related -->
<string name="error_code">código de error</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Haagi moodulid vaikimisi lahti</string>
<string name="settings_umount_modules_default_summary">Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile.</string>
<string name="profile_selinux_domain">Domeen</string>
<string name="profile_selinux_rules">Reeglid</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Umount modules by default</string>
<string name="settings_umount_modules_default_summary">The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Enabling this option will allow KernelSU to restore any modified files by the modules for this app.</string>
<string name="profile_selinux_domain">Domain</string>
<string name="profile_selinux_rules">Rules</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Umount modules by default</string>
<string name="settings_umount_modules_default_summary">Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Ang pagpapagana sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga modyul para sa aplikasyon na ito.</string>
<string name="profile_selinux_domain">Domain</string>
<string name="profile_selinux_rules">Mga Tuntunin</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">La version actuelle de KernelSU (%s) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %s ou à une version supérieure!</string>
<string name="settings_umount_modules_default">Démonter les modules par défaut</string>
<string name="settings_umount_modules_default_summary">Valeur globale par défaut pour l\'option \"Démonter les modules\" dans les profils d\'application. Lorsque l\'option est activée, les modifications apportées au système par les modules sont supprimées pour les applications qui n\'ont pas de profil défini.</string>
<string name="settings_susfs_toggle">Désactiver les crochets kprobe</string>
<string name="profile_umount_modules_summary">L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application.</string>
<string name="profile_selinux_domain">Domaine</string>
<string name="profile_selinux_rules">Règles</string>
@ -235,7 +234,6 @@
<string name="invalid_file_type">Type de fichier incorrect ! Veuillez sélectionner un fichier .kpm.</string>
<string name="confirm_uninstall_title_with_filename">Désinstaller</string>
<string name="confirm_uninstall_content">Le KPM suivant sera désinstallé : %s</string>
<string name="settings_susfs_toggle_summary">Désactivez les crochets kprobe créés par KernelSU, en utilisant des crochets en ligne à la place, ce qui est similaire à la méthode de crochet du noyau non-GKI.</string>
<string name="image_editor_hint">Utilisez deux doigts pour zoomer l\'image, et un doigt pour le faire glisser pour ajuster la position</string>
<string name="reprovision">Remise à disposition</string>
<!-- Kernel Flash Progress Related -->
@ -309,9 +307,8 @@
<string name="dpi_confirm_summary">L\'application doit être redémarrée pour appliquer les nouveaux paramètres de DPI, n\'affecte pas la barre d\'état du système ou d\'autres applications</string>
<string name="dpi_applied_success">Le DPI a été réglé sur %1$d, effectif après le redémarrage de l\'application</string>
<!-- Language settings related strings -->
<string name="language_setting">Langue de l\'application</string>
<string name="language_follow_system">Suivre le paramètre système</string>
<string name="language_changed">Langue modifiée, redémarrage pour appliquer les modifications</string>
<string name="settings_language">Langue de l\'application</string>
<string name="language_system_default">Suivre le paramètre système</string>
<string name="settings_card_dim">Ajustement de l\'obscurité de la carte</string>
<!-- Flash related -->
<string name="error_code">code d\'erreur</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें</string>
<string name="settings_umount_modules_default_summary">ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है।</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें।</string>
<string name="profile_selinux_domain">डोमेन</string>
<string name="profile_selinux_rules">नियम</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Umount module po zadanom</string>
<string name="settings_umount_modules_default_summary">Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju.</string>
<string name="profile_selinux_domain">Domena</string>
<string name="profile_selinux_rules">Pravila</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher!</string>
<string name="settings_umount_modules_default">Modulok leválasztása alapértelmezetten</string>
<string name="settings_umount_modules_default_summary">A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat.</string>
<string name="profile_selinux_domain">Tartomány</string>
<string name="profile_selinux_rules">Szabályok</string>
@ -233,7 +232,6 @@
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
@ -307,9 +305,8 @@
<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>
<!-- Language settings related strings -->
<string name="language_setting">App Language</string>
<string name="language_follow_system">Follow System</string>
<string name="language_changed">Language changed, restarting to apply changes</string>
<string name="settings_language">App Language</string>
<string name="language_system_default">Follow System</string>
<string name="settings_card_dim">Card Darkness Adjustment</string>
<!-- Flash related -->
<string name="error_code">error code</string>

View file

@ -0,0 +1,541 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home">Beranda</string>
<string name="home_not_installed">Tidak Terpasang</string>
<string name="home_click_to_install">Klik untuk Memasang</string>
<string name="home_working">Berfungsi</string>
<string name="home_working_version">Versi: %s</string>
<string name="home_unsupported">Tidak Didukung</string>
<string name="home_unsupported_reason">Driver KernelSU tidak terdeteksi di kernel Anda. Mungkin Anda menggunakan kernel yang salah.</string>
<string name="home_kernel">Versi Kernel</string>
<string name="home_susfs_version">Versi SuSFS</string>
<string name="home_manager_version">Versi Manajer</string>
<string name="home_selinux_status">Status SELinux</string>
<string name="selinux_status_disabled">Dinonaktifkan</string>
<string name="selinux_status_enforcing">Ditegakkan</string>
<string name="selinux_status_permissive">Permisi</string>
<string name="selinux_status_unknown">Tidak Diketahui</string>
<string name="superuser">Superuser</string>
<string name="module_failed_to_enable">Gagal mengaktifkan modul: %s</string>
<string name="module_failed_to_disable">Gagal menonaktifkan modul: %s</string>
<string name="module_empty">Tidak ada modul terpasang</string>
<string name="module">Modul</string>
<string name="module_sort_action_first">Urutkan (Aksi Terlebih Dahulu)</string>
<string name="module_sort_enabled_first">Urutkan (Aktif Terlebih Dahulu)</string>
<string name="uninstall">Copot Pemasangan</string>
<string name="module_install">Pasang</string>
<string name="install">Pasang</string>
<string name="reboot">Muat Ulang</string>
<string name="settings">Pengaturan</string>
<string name="reboot_userspace">Muat Ulang Lunak</string>
<string name="reboot_recovery">Muat Ulang ke Recovery</string>
<string name="reboot_bootloader">Muat Ulang ke Bootloader</string>
<string name="reboot_download">Muat Ulang ke Mode Download</string>
<string name="reboot_edl">Muat Ulang ke Mode EDL</string>
<string name="about">Tentang</string>
<string name="module_uninstall_confirm">Apakah Anda yakin ingin mencopot pemasangan modul %s?</string>
<string name="module_uninstall_success">%s telah dicopot</string>
<string name="module_uninstall_failed">Gagal mencopot pemasangan: %s</string>
<string name="module_version">Versi</string>
<string name="module_author">Penulis</string>
<string name="refresh">Segarkan</string>
<string name="show_system_apps">Tampilkan Aplikasi Sistem</string>
<string name="hide_system_apps">Sembunyikan Aplikasi Sistem</string>
<string name="send_log">Kirim Log</string>
<string name="safe_mode">Mode Aman</string>
<string name="reboot_to_apply">Muat ulang untuk menerapkan</string>
<string name="module_magisk_conflict">Modul tidak tersedia karena konflik dengan Magisk!</string>
<string name="home_learn_kernelsu">Pelajari tentang KernelSU</string>
<string name="home_learn_kernelsu_url">https://kernelsu.org/guide/what-is-kernelsu.html</string>
<string name="home_click_to_learn_kernelsu">Pelajari cara memasang KernelSU dan menggunakan modul</string>
<string name="home_support_title">Dukung Kami</string>
<string name="home_support_content">KernelSU bersifat gratis dan open source, sekarang dan selamanya. Namun, Anda dapat menunjukkan dukungan Anda dengan melakukan donasi.</string>
<string name="about_source_code"><![CDATA[Lihat kode sumber di %1$s<br/>Gabung ke saluran %2$s kami]]></string>
<string name="profile">Profil Aplikasi</string>
<string name="profile_default">Bawaan</string>
<string name="profile_template">Templat</string>
<string name="profile_custom">Khusus</string>
<string name="profile_name">Nama Profil</string>
<string name="profile_groups">Grup</string>
<string name="profile_capabilities">Kemampuan</string>
<string name="profile_selinux_context">Konteks SELinux</string>
<string name="profile_umount_modules">Lepas Kait Modul</string>
<string name="failed_to_update_app_profile">Gagal memperbarui profil aplikasi untuk %s</string>
<string name="require_kernel_version" formatted="false">Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manajer dengan benar. Harap perbarui ke versi %s atau yang lebih tinggi!</string>
<string name="settings_umount_modules_default">Lepas kait modul secara bawaan</string>
<string name="settings_umount_modules_default_summary">Nilai bawaan global untuk \"Lepas Kait Modul\" dalam profil aplikasi. Jika diaktifkan, ini akan menghapus semua perubahan sistem yang dibuat oleh modul untuk aplikasi tanpa profil yang ditetapkan.</string>
<string name="profile_umount_modules_summary">Mengaktifkan opsi ini akan memungkinkan KernelSU untuk memulihkan file yang diubah oleh modul untuk aplikasi ini.</string>
<string name="profile_selinux_domain">Domain</string>
<string name="profile_selinux_rules">Aturan</string>
<string name="module_update">Perbarui</string>
<string name="module_downloading">Mengunduh modul: %s</string>
<string name="module_start_downloading">Memulai pengunduhan: %s</string>
<string name="new_version_available">Versi baru %s tersedia, klik untuk memperbarui.</string>
<string name="launch_app">Jalankan</string>
<string name="force_stop_app" formatted="false">Paksa Hentikan</string>
<string name="restart_app">Jalankan Ulang</string>
<string name="failed_to_update_sepolicy">Gagal memperbarui aturan SELinux untuk %s</string>
<string name="module_changelog">Catatan Perubahan</string>
<string name="settings_profile_template">Templat Profil Aplikasi</string>
<string name="settings_profile_template_summary">Kelola templat profil aplikasi lokal dan daring</string>
<string name="app_profile_template_create">Buat Templat</string>
<string name="app_profile_template_edit">Edit Templat</string>
<string name="app_profile_template_id">ID</string>
<string name="app_profile_template_id_invalid">ID Templat Tidak Valid</string>
<string name="app_profile_template_name">Nama</string>
<string name="app_profile_template_description">Deskripsi</string>
<string name="app_profile_template_save">Simpan</string>
<string name="app_profile_template_delete">Hapus</string>
<string name="app_profile_template_view">Lihat Templat</string>
<string name="app_profile_template_readonly">Hanya Baca</string>
<string name="app_profile_template_id_exist">ID Templat sudah ada!</string>
<string name="app_profile_import_export">Impor/Ekspor</string>
<string name="app_profile_import_from_clipboard">Impor dari Papan Klip</string>
<string name="app_profile_export_to_clipboard">Ekspor ke Papan Klip</string>
<string name="app_profile_template_export_empty">Tidak ditemukan templat lokal untuk diekspor!</string>
<string name="app_profile_template_import_success">Berhasil diimpor</string>
<string name="app_profile_template_sync">Sinkronkan Templat Daring</string>
<string name="app_profile_template_save_failed">Gagal menyimpan templat</string>
<string name="app_profile_template_import_empty">Papan klip kosong!</string>
<string name="module_changelog_failed">Gagal memuat catatan perubahan: %s</string>
<string name="settings_check_update">Periksa Pembaruan</string>
<string name="settings_check_update_summary">Secara otomatis memeriksa pembaruan saat membuka aplikasi</string>
<string name="grant_root_failed">Gagal memberikan hak akses root!</string>
<string name="action">Aksi</string>
<string name="close">Tutup</string>
<string name="enable_web_debugging">Aktifkan Debug WebView</string>
<string name="enable_web_debugging_summary">Dapat digunakan untuk mendebug WebUI. Harap aktifkan hanya jika diperlukan.</string>
<string name="direct_install">Pemasangan Langsung (Disarankan)</string>
<string name="select_file">Pilih Gambar untuk Dipatch</string>
<string name="install_inactive_slot">Pasang ke Slot Tidak Aktif (Setelah OTA)</string>
<string name="install_inactive_slot_warning">Perangkat Anda akan **DIPAKSA** untuk boot ke slot tidak aktif saat ini setelah reboot!
Gunakan opsi ini hanya setelah OTA selesai.
Lanjutkan?</string>
<string name="install_next">Lanjut</string>
<string name="select_file_tip">Disarankan gambar partisi %1$s</string>
<string name="select_file_tip_vendor">(tidak stabil)</string>
<string name="select_kmi">Pilih KMI</string>
<string name="settings_uninstall">Copot Pemasangan</string>
<string name="settings_uninstall_temporary">Copot Pemasangan Sementara</string>
<string name="settings_uninstall_permanent">Copot Pemasangan Permanen</string>
<string name="settings_restore_stock_image">Pulihkan Gambar Bawaan</string>
<string name="settings_uninstall_temporary_message">Copot pemasangan KernelSU secara sementara, kembalikan ke keadaan awal setelah reboot berikutnya.</string>
<string name="settings_uninstall_permanent_message">Copot pemasangan KernelSU secara lengkap dan permanen (Root dan semua modul).</string>
<string name="settings_restore_stock_image_message">Pulihkan gambar bawaan pabrik (jika cadangan tersedia), biasanya digunakan sebelum OTA; jika ingin mencopot KernelSU, gunakan \"Copot Pemasangan Permanen\".</string>
<string name="flashing">Mem-flash</string>
<string name="flash_success">Flash Berhasil</string>
<string name="flash_failed">Flash Gagal</string>
<string name="selected_lkm">LKM Terpilih: %s</string>
<string name="save_log">Simpan Log</string>
<string name="log_saved">Log Disimpan</string>
<string name="sus_su_mode">Mode SuS SU:</string>
<!-- Module related -->
<string name="module_install_confirm">Konfirmasi pemasangan modul %1$s?</string>
<string name="unknown_module">modul tidak dikenal</string>
<!-- Restore related -->
<string name="restore_confirm_title">Konfirmasi Pemulihan Modul</string>
<string name="restore_confirm_message">Operasi ini akan menimpa semua modul yang ada. Lanjutkan?</string>
<string name="confirm">Konfirmasi</string>
<string name="cancel">Batal</string>
<!-- Backup related -->
<string name="backup_success">Pencadangan Berhasil (tar.gz)</string>
<string name="backup_failed">Gagal membuat cadangan: %1$s</string>
<string name="backup_modules">cadangan modul</string>
<string name="restore_modules">pulihkan modul</string>
<!-- Restore related messages -->
<string name="restore_success">Modul berhasil dipulihkan, perlu reboot</string>
<string name="restore_failed">Gagal memulihkan: %1$s</string>
<string name="restart_now">Muat Ulang Sekarang</string>
<string name="unknown_error">Kesalahan Tidak Diketahui</string>
<!-- Command related -->
<string name="command_execution_failed">Gagal mengeksekusi perintah: %1$s</string>
<!-- Allowlist related -->
<string name="allowlist_backup_success">Pencadangan daftar izin berhasil</string>
<string name="allowlist_backup_failed">Gagal membuat cadangan daftar izin: %1$s</string>
<string name="allowlist_restore_confirm_title">Konfirmasi Pemulihan Daftar Izin</string>
<string name="allowlist_restore_confirm_message">Operasi ini akan menimpa daftar izin saat ini. Lanjutkan?</string>
<string name="allowlist_restore_success">Daftar izin berhasil dipulihkan</string>
<string name="allowlist_restore_failed">Gagal memulihkan daftar izin: %1$s</string>
<string name="backup_allowlist">Cadangkan Daftar Izin</string>
<string name="restore_allowlist">Pulihkan Daftar Izin</string>
<string name="settings_custom_background">Latar Belakang Aplikasi Khusus</string>
<string name="settings_custom_background_summary">Pilih gambar sebagai latar belakang</string>
<string name="settings_card_alpha">Transparansi Panel Navigasi</string>
<string name="home_android_version">Versi Android</string>
<string name="home_device_model">Model Perangkat</string>
<string name="su_not_allowed">Pemberian hak superuser untuk %s tidak diizinkan</string>
<string name="settings_disable_su">Nonaktifkan Kompatibilitas su</string>
<string name="settings_disable_su_summary">Sementara mencegah aplikasi mana pun mendapatkan hak root melalui perintah su (proses root yang ada tidak akan terpengaruh).</string>
<string name="module_install_multiple_confirm_with_names">Apakah Anda yakin ingin memasang %1$d modul berikut?
%2$s</string>
<string name="more_settings">Pengaturan Lainnya</string>
<string name="selinux">SELinux</string>
<string name="selinux_enabled">Diaktifkan</string>
<string name="selinux_disabled">Dinonaktifkan</string>
<string name="simple_mode">Mode Sederhana</string>
<string name="simple_mode_summary">Menyembunyikan kartu yang tidak perlu saat diaktifkan</string>
<string name="hide_kernel_kernelsu_version">Sembunyikan Versi Kernel</string>
<string name="hide_kernel_kernelsu_version_summary">Menyembunyikan versi kernel</string>
<string name="hide_other_info">Sembunyikan Informasi Lainnya</string>
<string name="hide_other_info_summary">Menyembunyikan titik merah yang menunjukkan jumlah superuser, modul, dan modul KPM di halaman navigasi bawah</string>
<string name="hide_susfs_status">Sembunyikan Status SuSFS</string>
<string name="hide_susfs_status_summary">Menyembunyikan informasi status SuSFS di halaman beranda</string>
<string name="hide_link_card">Sembunyikan Kartu Tautan</string>
<string name="hide_link_card_summary">Menyembunyikan informasi di kartu tautan di halaman beranda</string>
<string name="hide_tag_card">Sembunyikan Baris Tag Modul</string>
<string name="hide_tag_card_summary">Menyembunyikan label nama folder dan ukuran di kartu modul</string>
<string name="theme_mode">Tema</string>
<string name="theme_follow_system">Ikuti Sistem</string>
<string name="theme_light">Terang</string>
<string name="theme_dark">Gelap</string>
<string name="manual_hook">Hook Manual</string>
<string name="dynamic_color_title">Warna Dinamis</string>
<string name="dynamic_color_summary">Warna dinamis menggunakan tema sistem</string>
<string name="choose_theme_color">Pilih Warna Tema</string>
<string name="color_default">Biru</string>
<string name="color_green">Hijau</string>
<string name="color_purple">Ungu</string>
<string name="color_orange">Oranye</string>
<string name="color_pink">Merah Muda</string>
<string name="color_gray">Abu-abu</string>
<string name="color_yellow">Kuning</string>
<string name="horizon_kernel">Pasang Anykernel3</string>
<string name="horizon_kernel_summary">Flash file kernel AnyKernel3</string>
<string name="root_required">Diperlukan hak akses root</string>
<string name="reboot_complete_title">Pembersihan Selesai</string>
<string name="reboot_complete_msg">Muat ulang sekarang?</string>
<string name="yes">Ya</string>
<string name="no">Tidak</string>
<string name="failed_reboot">Gagal memuat ulang</string>
<string name="kpm_title">KPM</string>
<string name="kpm_empty">Saat ini tidak ada modul kernel yang terpasang</string>
<string name="kpm_version">Versi</string>
<string name="kpm_author">Penulis</string>
<string name="kpm_uninstall">Copot Pemasangan</string>
<string name="kpm_uninstall_success">Berhasil dicopot</string>
<string name="kpm_uninstall_failed">Gagal mencopot</string>
<string name="kpm_install_success">Berhasil memuat modul kpm</string>
<string name="kpm_install_failed">Gagal memuat modul kpm</string>
<string name="kpm_args">Parameter</string>
<string name="kpm_control">Jalankan</string>
<string name="home_kpm_version">Versi KPM</string>
<string name="close_notice">Tutup</string>
<string name="kernel_module_notice">Fitur modul kernel berikut dikembangkan oleh KernelPatch dan dimodifikasi untuk menyertakan fitur modul kernel SukiSU Ultra</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra menantikan</string>
<string name="kpm_control_success">Berhasil</string>
<string name="kpm_control_failed">Gagal</string>
<string name="home_click_to_ContributionCard_kernelsu">Ke depannya, SukiSU Ultra akan menjadi cabang KSU yang relatif independen, tetapi kami tetap berterima kasih kepada KernelSU resmi, MKSU, dan lainnya atas kontribusi mereka!</string>
<string name="not_supported">Tidak Didukung</string>
<string name="supported">Didukung</string>
<string name="kernel_patched">Kernel Belum Di-patch</string>
<string name="kernel_not_enabled">Kernel Belum Diaktifkan</string>
<string name="custom_settings">Pengaturan Khusus</string>
<string name="kpm_install_mode">Pemasangan KPM</string>
<string name="kpm_install_mode_load">Muat</string>
<string name="kpm_install_mode_embed">Tanamkan</string>
<string name="kpm_install_mode_description">Silakan pilih: %1$s mode pemasangan modul
Muat: Secara sementara memuat modul
Tanamkan: Secara permanen memasang ke sistem</string>
<string name="snackbar_failed_to_check_module_file">Gagal memeriksa keberadaan file modul</string>
<string name="theme_color">Warna Tema</string>
<string name="invalid_file_type">Jenis file tidak valid! Harap pilih file .kpm.</string>
<string name="confirm_uninstall_title_with_filename">Copot Pemasangan</string>
<string name="confirm_uninstall_content">Akan mencopot KPM berikut: %s</string>
<string name="image_editor_hint">Gunakan dua jari untuk memperbesar gambar dan satu jari untuk menyeret, untuk menyesuaikan posisi</string>
<string name="reprovision">Provisi Ulang</string>
<!-- Kernel Flash Progress Related -->
<string name="horizon_flash_complete">Flash Selesai</string>
<!-- Flash Status Related -->
<string name="horizon_preparing">Menyiapkan…</string>
<string name="horizon_cleaning_files">Membersihkan file…</string>
<string name="horizon_copying_files">Menyalin file…</string>
<string name="horizon_extracting_tool">Mengekstrak alat flash…</string>
<string name="horizon_patching_script">Menambal skrip flash…</string>
<string name="horizon_flashing">Mem-flash kernel…</string>
<string name="horizon_flash_complete_status">Flash Selesai</string>
<!-- Slot selection related strings -->
<string name="select_slot_title">Pilih Slot untuk Flash</string>
<string name="select_slot_description">Silakan pilih slot target untuk flashing boot</string>
<string name="slot_a">Slot A</string>
<string name="slot_b">Slot B</string>
<string name="selected_slot">Slot Terpilih: %1$s</string>
<string name="horizon_getting_original_slot">Mendapatkan slot asli</string>
<string name="horizon_setting_target_slot">Mengatur slot target</string>
<string name="horizon_restoring_original_slot">Mengembalikan slot bawaan</string>
<string name="current_slot">Slot sistem bawaan saat ini: %1$s</string>
<!-- Error Messages -->
<string name="horizon_copy_failed">Gagal menyalin</string>
<string name="horizon_unknown_error">Kesalahan Tidak Diketahui</string>
<string name="flash_failed_message">Flash Gagal</string>
<!-- lkm/gki install -->
<string name="Lkm_install_methods">Pemulihan/Pemasangan LKM</string>
<string name="GKI_install_methods">Flash AnyKernel3</string>
<string name="kernel_version_log">Versi Kernel: %1$s</string>
<string name="tool_version_log">Alat patch yang digunakan: %1$s</string>
<string name="configuration">Konfigurasi</string>
<string name="app_settings">Pengaturan Aplikasi</string>
<string name="tools">Alat</string>
<!-- String resources used in SuperUser -->
<string name="no_apps_found">Aplikasi tidak ditemukan</string>
<string name="selinux_enabled_toast">SELinux diaktifkan</string>
<string name="selinux_disabled_toast">SELinux dinonaktifkan</string>
<string name="selinux_change_failed">Gagal mengubah status SELinux</string>
<string name="advanced_settings">Pengaturan Lanjutan</string>
<string name="appearance_settings">Sesuaikan Bilah Alat</string>
<string name="back">Kembali</string>
<string name="susfs_enabled">SuSFS diaktifkan</string>
<string name="susfs_disabled">SuSFS dinonaktifkan</string>
<string name="background_set_success">Latar belakang berhasil diatur</string>
<string name="background_removed">Latar belakang khusus dihapus</string>
<string name="icon_switch_title">Ikon Alternatif</string>
<string name="icon_switch_summary">Ubah ikon peluncur menjadi ikon KernelSU.</string>
<string name="icon_switched">Ikon diubah</string>
<!-- KPM display settings -->
<string name="show_kpm_info">Sembunyikan Fungsi KPM</string>
<string name="show_kpm_info_summary">Menyembunyikan informasi dan fungsi KPM di layar utama dan panel bawah</string>
<!-- Webui X settings -->
<string name="use_webuix">Pilih Mesin WebUI untuk Digunakan</string>
<string name="engine_auto_select">Pilih Otomatis</string>
<string name="engine_force_webuix">Paksa Gunakan WebUI X</string>
<string name="engine_force_ksu">Paksa Gunakan KSU WebUI</string>
<string name="use_webuix_eruda">Sisipkan Eruda ke WebUI X</string>
<string name="use_webuix_eruda_summary">Sisipkan konsol debug ke WebUI X untuk memudahkan debugging. Memerlukan debugging web diaktifkan.</string>
<!-- DPI setting related strings -->
<string name="app_dpi_title">DPI yang Diterapkan</string>
<string name="app_dpi_summary">Sesuaikan kepadatan layar hanya untuk aplikasi saat ini</string>
<string name="dpi_size_small">Kecil</string>
<string name="dpi_size_medium">Sedang</string>
<string name="dpi_size_large">Besar</string>
<string name="dpi_size_extra_large">Sangat Besar</string>
<string name="dpi_size_custom">Khusus</string>
<string name="dpi_apply_settings">Menerapkan Pengaturan DPI</string>
<string name="dpi_confirm_title">Konfirmasi Perubahan DPI</string>
<string name="dpi_confirm_message">Apakah Anda yakin ingin mengubah DPI aplikasi dari %1$d menjadi %2$d?</string>
<string name="dpi_confirm_summary">Aplikasi perlu dijalankan ulang agar pengaturan DPI baru diterapkan; ini tidak akan mempengaruhi bilah status sistem atau aplikasi lainnya</string>
<string name="dpi_applied_success">DPI diatur ke %1$d, akan diterapkan setelah aplikasi dijalankan ulang</string>
<!-- Language settings related strings -->
<string name="settings_language">Bahasa Aplikasi</string>
<string name="language_system_default">Ikuti Sistem</string>
<string name="settings_card_dim">Pengaturan Pencahayaan Kartu</string>
<!-- Flash related -->
<string name="error_code">kode kesalahan</string>
<string name="check_log">Silakan periksa log</string>
<string name="installing_module">Memasang modul %1$d/%2$d</string>
<string name="module_failed_count">Gagal memasang %d modul baru</string>
<string name="module_download_error">Gagal mengunduh modul</string>
<string name="kernel_flashing">Mem-flash Kernel</string>
<!-- 分类相关 -->
<string name="category_all_apps">Semua</string>
<string name="category_root_apps">Root</string>
<string name="category_custom_apps">Khusus</string>
<string name="category_default_apps">Bawaan</string>
<!-- 排序相关 -->
<string name="sort_name_asc">Nama Naik</string>
<string name="sort_name_desc">Nama Turun</string>
<string name="sort_install_time_new">Waktu Pemasangan (Baru)</string>
<string name="sort_install_time_old">Waktu Pemasangan (Lama)</string>
<string name="sort_size_desc">Ukuran Turun</string>
<string name="sort_size_asc">Ukuran Naik</string>
<string name="sort_usage_freq">Frekuensi Penggunaan</string>
<!-- 状态相关 -->
<string name="no_apps_in_category">Tidak ada aplikasi dalam kategori ini</string>
<!-- 标签相关 -->
<string name="deny_authorization">Tolak Hak Akses</string>
<string name="grant_authorization">Berikan Hak Akses</string>
<string name="unmount_modules">Lepas Kaitan Modul</string>
<string name="disable_unmount">Nonaktifkan Lepas Kaitan Modul</string>
<string name="expand_menu">Perluas Menu</string>
<string name="collapse_menu">Ciutkan Menu</string>
<string name="scroll_to_top">Ke Atas</string>
<string name="scroll_to_bottom">Ke Bawah</string>
<string name="selected">Terpilih</string>
<string name="select">Pilih</string>
<!-- BottomSheet相关 -->
<string name="menu_options">Opsi Menu</string>
<string name="sort_options">Urutkan Berdasarkan</string>
<string name="app_categories">Pilih Jenis Aplikasi</string>
<!-- SuSFS Configuration -->
<string name="susfs_config_title">Konfigurasi SuSFS</string>
<string name="susfs_config_description">Deskripsi Konfigurasi</string>
<string name="susfs_config_description_text">Fitur ini memungkinkan Anda untuk mengonfigurasi spoofing nilai uname dan waktu build SuSFS. Masukkan nilai yang diinginkan dan klik \"Terapkan\" agar berlaku.</string>
<string name="susfs_uname_label">Nilai Uname</string>
<string name="susfs_uname_placeholder">Silakan masukkan nilai uname khusus</string>
<string name="susfs_build_time_label">Spoof Waktu Build</string>
<string name="susfs_build_time_placeholder">Silakan masukkan nilai spoof waktu build</string>
<string name="susfs_current_value">Nilai Saat Ini: %s</string>
<string name="susfs_current_build_time">Waktu Build Saat Ini: %s</string>
<string name="susfs_reset_to_default">Atur Ulang ke Bawaan</string>
<string name="susfs_apply">Terapkan</string>
<!-- SuSFS Reset Confirmation -->
<string name="susfs_reset_confirm_title">Konfirmasi Atur Ulang</string>
<!-- SuSFS Toast Messages -->
<string name="susfs_binary_not_found">Gagal menemukan file ksu_susfs</string>
<string name="susfs_command_failed">Gagal mengeksekusi perintah SuSFS</string>
<string name="susfs_command_error">Kesalahan eksekusi perintah SuSFS: %s</string>
<string name="susfs_uname_set_success" formatted="false">Nilai uname dan waktu build SuSFS berhasil diatur: %s, %s</string>
<!-- SuSFS Settings Item -->
<string name="susfs_config_setting_title">Konfigurasi SuSFS</string>
<!-- 开机自启动相关 -->
<string name="susfs_autostart_title">Mulai Otomatis</string>
<string name="susfs_autostart_description">Secara otomatis menerapkan semua konfigurasi non-bawaan saat reboot</string>
<string name="susfs_autostart_requirement">Perlu menambahkan konfigurasi untuk mengaktifkan</string>
<string name="susfs_autostart_enable_failed">Gagal mengaktifkan mulai otomatis</string>
<string name="susfs_autostart_disable_failed">Gagal menonaktifkan mulai otomatis</string>
<string name="susfs_autostart_error">Kesalahan konfigurasi mulai otomatis: %s</string>
<string name="susfs_no_config_to_autostart">Tidak ada konfigurasi yang tersedia untuk mulai otomatis</string>
<!-- SuSFS Tab Titles -->
<string name="susfs_tab_basic_settings">Pengaturan Dasar</string>
<string name="susfs_tab_sus_paths">Jalur SUS</string>
<string name="susfs_tab_sus_mounts">Kaitan SUS</string>
<string name="susfs_tab_try_umount">Coba Lepas Kait</string>
<string name="susfs_tab_path_settings">Pengaturan Jalur</string>
<string name="susfs_tab_enabled_features">Status Fitur Diaktifkan</string>
<!-- SuSFS Path Management -->
<string name="susfs_add_sus_path">Tambah Jalur SUS</string>
<string name="susfs_add_sus_mount">Tambah Kaitan SUS</string>
<string name="susfs_add_try_umount">Tambah Coba Lepas Kait</string>
<string name="susfs_sus_path_added_success">Jalur SUS berhasil ditambahkan</string>
<string name="susfs_path_not_found_error">Kesalahan: Jalur tidak ditemukan</string>
<string name="susfs_path_label">Jalur</string>
<string name="susfs_mount_path_label">Jalur Kaitan</string>
<string name="susfs_path_placeholder">misalnya: /system/addon.d</string>
<string name="susfs_no_paths_configured">Tidak ada jalur SUS yang dikonfigurasi</string>
<string name="susfs_no_mounts_configured">Tidak ada kaitan SUS yang dikonfigurasi</string>
<string name="susfs_no_umounts_configured">Tidak ada coba lepas kait yang dikonfigurasi</string>
<!-- SuSFS Umount Mode -->
<string name="susfs_umount_mode_label">Mode Lepas Kait</string>
<string name="susfs_umount_mode_normal">Lepas Kait Normal (0)</string>
<string name="susfs_umount_mode_detach">Lepas Kait Terpisah (1)</string>
<string name="susfs_umount_mode_normal_short">Normal</string>
<string name="susfs_umount_mode_detach_short">Terpisah</string>
<string name="susfs_umount_mode_display">Mode: %1$s (%2$s)</string>
<string name="susfs_try_umount_added_success">Jalur coba lepas kait berhasil ditambahkan: %s</string>
<string name="susfs_try_umount_added_saved">Berhasil menyimpan jalur coba lepas kait: %s</string>
<!-- SuSFS Run Umount -->
<!-- SuSFS Reset Categories -->
<string name="susfs_reset_paths_title">Atur Ulang Jalur SUS</string>
<string name="susfs_reset_paths_message">Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan?</string>
<string name="susfs_reset_mounts_title">Atur Ulang Kaitan SUS</string>
<string name="susfs_reset_mounts_message">Ini akan menghapus semua konfigurasi kaitan SUS. Apakah Anda yakin ingin melanjutkan?</string>
<string name="susfs_reset_umounts_title">Atur Ulang Coba Lepas Kait</string>
<string name="susfs_reset_umounts_message">Ini akan menghapus semua konfigurasi coba lepas kait. Apakah Anda yakin ingin melanjutkan?</string>
<string name="susfs_reset_path_title">Atur Ulang Pengaturan Jalur</string>
<!-- SuSFS Path Settings -->
<string name="susfs_android_data_path_label">Jalur Data Android</string>
<string name="susfs_sdcard_path_label">Jalur Kartu SD</string>
<string name="susfs_set_android_data_path">Atur Jalur Data Android</string>
<string name="susfs_set_sdcard_path">Atur Jalur Kartu SD</string>
<!-- SuSFS Enabled Features -->
<string name="susfs_enabled_features_description">Menampilkan status saat ini dari fitur SuSFS yang diaktifkan</string>
<string name="susfs_no_features_found">Informasi status fitur tidak ditemukan</string>
<string name="susfs_feature_enabled">Diaktifkan</string>
<string name="susfs_feature_disabled">Dinonaktifkan</string>
<!-- Feature Labels -->
<string name="sus_path_feature_label">Dukungan Jalur SUS</string>
<string name="sus_mount_feature_label">Dukungan Kaitan SUS</string>
<string name="try_umount_feature_label">Dukungan Coba Lepas Kait</string>
<string name="spoof_uname_feature_label">Dukungan Spoof Uname</string>
<string name="spoof_cmdline_feature_label">Spoof Cmdline/Bootconfig</string>
<string name="open_redirect_feature_label">Dukungan Open Redirect</string>
<string name="enable_log_feature_label">Dukungan Logging</string>
<string name="auto_default_mount_feature_label">Kaitan Bawaan Otomatis</string>
<string name="auto_bind_mount_feature_label">Kaitan Bind Otomatis</string>
<string name="auto_try_umount_bind_feature_label">Coba Lepas Kaitan Bind Otomatis</string>
<string name="hide_symbols_feature_label">Sembunyikan Simbol KSU SUSFS</string>
<string name="sus_kstat_feature_label">Dukungan SUS Kstat</string>
<string name="sus_su_feature_label">Fitur Toggle Mode SUS SU</string>
<!-- 可切换状态 -->
<string name="susfs_feature_configurable">Fitur SuSFS yang Dapat Dikonfigurasi</string>
<string name="susfs_enable_log_label">Aktifkan Log SuSFS</string>
<string name="susfs_log_config_description">Aktifkan atau nonaktifkan logging untuk SuSFS</string>
<string name="susfs_log_config_title">Pengaturan Logging SuSFS</string>
<string name="susfs_log_enabled">Mengaktifkan Logging SuSFS</string>
<string name="susfs_log_disabled">Menonaktifkan Logging SuSFS</string>
<string name="module_update_json">Perbarui JSON</string>
<string name="module_update_json_copied">URL Perbarui JSON disalin ke papan klip</string>
<!-- Settings related strings -->
<string name="show_more_module_info">Tampilkan Informasi Modul Lebih Banyak</string>
<string name="show_more_module_info_summary">Tampilkan informasi modul tambahan seperti URL perbarui JSON</string>
<string name="susfs_execution_location_label">Lokasi Eksekusi</string>
<string name="susfs_current_execution_location">Lokasi Eksekusi Saat Ini: %s</string>
<string name="susfs_execution_location_service">Layanan</string>
<string name="susfs_execution_location_post_fs_data">Post-FS-Data</string>
<string name="susfs_execution_location_service_description">Jalankan setelah layanan sistem dimulai</string>
<string name="susfs_execution_location_post_fs_data_description">Jalankan setelah sistem file dikaitkan tetapi sebelum sistem sepenuhnya dinyalakan. Dapat menyebabkan bootloop</string>
<string name="susfs_slot_info_title">Informasi Slot</string>
<string name="susfs_slot_info_description">Lihat informasi slot boot saat ini dan salin nilainya</string>
<string name="susfs_current_active_slot">Slot Aktif Saat Ini: %s</string>
<string name="susfs_slot_uname">Uname: %s</string>
<string name="susfs_slot_build_time">Waktu Build: %s</string>
<string name="susfs_slot_current_badge">Saat Ini</string>
<string name="susfs_slot_use_uname">Gunakan Uname</string>
<string name="susfs_slot_use_build_time">Gunakan Waktu Build</string>
<string name="susfs_slot_info_unavailable">Gagal mendapatkan informasi slot</string>
<!-- SuSFS 自启动相关字符串 -->
<string name="susfs_autostart_enabled_success">Modul mulai otomatis SuSFS diaktifkan, jalur modul: %s</string>
<string name="susfs_autostart_disabled_success">Modul mulai otomatis SuSFS dinonaktifkan</string>
<!-- SuSFS Kstat相关字符串 -->
<string name="susfs_tab_kstat_config">Konfigurasi Kstat</string>
<string name="kstat_static_config_added">Konfigurasi Kstat statis ditambahkan: %1$s</string>
<string name="kstat_config_removed">Konfigurasi Kstat dihapus: %1$s</string>
<string name="kstat_path_added">Jalur Kstat ditambahkan: %1$s</string>
<string name="kstat_path_removed">Jalur Kstat dihapus: %1$s</string>
<string name="kstat_updated">Kstat diperbarui: %1$s</string>
<string name="kstat_full_clone_updated">Klon Lengkap Kstat diperbarui: %1$s</string>
<string name="add_kstat_statically_title">Tambahkan Konfigurasi Kstat Statis</string>
<string name="file_or_directory_path_label">Jalur File/Direktori</string>
<string name="hint_use_default_value">Petunjuk: Anda dapat menggunakan \"default\" untuk menggunakan nilai asli</string>
<string name="add_kstat_path_title">Tambah Jalur Kstat</string>
<string name="add">Tambah</string>
<string name="reset_kstat_config_title">Atur Ulang Konfigurasi Kstat</string>
<string name="reset_kstat_config_message">Apakah Anda yakin ingin membersihkan semua konfigurasi Kstat? Tindakan ini tidak dapat dibatalkan.</string>
<string name="kstat_config_description_title">Deskripsi Konfigurasi Kstat</string>
<string name="kstat_config_description_add_statically">• add_sus_kstat_statically: Informasi file/direktori statis</string>
<string name="kstat_config_description_add">• add_sus_kstat: Tambahkan jalur sebelum bind mount, menjaga informasi asli</string>
<string name="kstat_config_description_update">• update_sus_kstat: Perbarui ino target, membiarkan ukuran dan blok tidak berubah</string>
<string name="kstat_config_description_update_full_clone">• update_sus_kstat_full_clone: Perbarui hanya ino, membiarkan nilai asli lainnya</string>
<string name="static_kstat_config">Konfigurasi Kstat Statis</string>
<string name="kstat_path_management">Manajemen Jalur Kstat</string>
<string name="no_kstat_config_message">Belum ada konfigurasi Kstat, klik tombol di atas untuk menambahkan</string>
<!-- SuSFS Mount Hiding Control Related Strings -->
<string name="susfs_hide_mounts_control_title">Kontrol Penyembunyian Kaitan SUS</string>
<string name="susfs_hide_mounts_control_description">Kontrol perilaku penyembunyian kaitan SUS untuk proses</string>
<string name="susfs_hide_mounts_for_all_procs_label">Sembunyikan Kaitan SUS untuk Semua Proses</string>
<string name="susfs_hide_mounts_for_all_procs_enabled_description">Jika diaktifkan, kaitan SUS akan disembunyikan dari semua proses, termasuk proses KSU</string>
<string name="susfs_hide_mounts_for_all_procs_disabled_description">Jika dinonaktifkan, kaitan SUS akan disembunyikan hanya dari proses non-KSU; proses KSU akan dapat melihat kaitan</string>
<string name="susfs_hide_mounts_all_enabled">Mengaktifkan penyembunyian kaitan SUS untuk semua proses</string>
<string name="susfs_hide_mounts_all_disabled">Menonaktifkan penyembunyian kaitan SUS untuk semua proses</string>
<string name="susfs_hide_mounts_recommendation">Disarankan untuk mengatur ke nonaktif setelah layar terbuka atau pada tahap service.sh atau boot-completed.sh, karena ini seharusnya memperbaiki masalah dengan beberapa aplikasi root yang bergantung pada kaitan yang dibuat oleh proses KSU</string>
<string name="susfs_hide_mounts_current_setting">Pengaturan Saat Ini: %s</string>
<string name="susfs_hide_mounts_setting_all">Sembunyikan untuk Semua Proses</string>
<string name="susfs_hide_mounts_setting_non_ksu">Sembunyikan Hanya untuk Proses Non-KSU</string>
<string name="kernel_simple_kernel">Mode Versi Kernel Sederhana</string>
<string name="kernel_simple_kernel_summary">Aktifkan atau nonaktifkan tampilan versi kernel SukiSU sederhana</string>
<string name="susfs_android_data_path_set">Jalur Data Android diatur ke: %s</string>
<string name="susfs_sdcard_path_set">Jalur Kartu SD diatur ke: %s</string>
<string name="susfs_path_setup_warning">Pengaturan jalur mungkin tidak sepenuhnya berhasil, tetapi jalur SUS akan tetap ditambahkan</string>
<!-- 备份和还原相关字符串 -->
<string name="susfs_backup_title">Cadangan</string>
<string name="susfs_backup_description">Buat cadangan semua konfigurasi SuSFS. File cadangan akan menyertakan semua pengaturan, jalur, dan konfigurasi.</string>
<string name="susfs_backup_create">Buat Cadangan</string>
<string name="susfs_backup_success">Berhasil membuat cadangan: %s</string>
<string name="susfs_backup_failed">Gagal membuat cadangan: %s</string>
<string name="susfs_backup_file_not_found">File cadangan tidak ditemukan</string>
<string name="susfs_backup_invalid_format">Format file cadangan tidak valid</string>
<string name="susfs_backup_version_mismatch">Versi cadangan tidak cocok, tetapi akan dicoba untuk dipulihkan</string>
<string name="susfs_restore_title">Pulihkan</string>
<string name="susfs_restore_description">Pulihkan konfigurasi SuSFS dari file cadangan. Ini akan menimpa semua pengaturan saat ini.</string>
<string name="susfs_restore_select_file">Pilih File Cadangan</string>
<string name="susfs_restore_success" formatted="false">Konfigurasi berhasil dipulihkan dari cadangan yang dibuat %s pada perangkat: %s</string>
<string name="susfs_restore_failed">Gagal memulihkan: %s</string>
<string name="susfs_restore_confirm_title">Konfirmasi Pemulihan</string>
<string name="susfs_restore_confirm_description">Ini akan menimpa semua konfigurasi SuSFS saat ini. Apakah Anda yakin ingin melanjutkan?</string>
<string name="susfs_restore_confirm">Pulihkan</string>
<string name="susfs_backup_info_date">Tanggal Cadangan: %s</string>
<string name="susfs_backup_info_device">Perangkat: %s</string>
<string name="susfs_backup_info_version">Versi: %s</string>
<string name="hide_bl_script">Status Terkunci</string>
<string name="hide_bl_script_description">Timpa properti status bootloader dalam layanan late_start</string>
<string name="cleanup_residue">Bersihkan Sisa-sisa</string>
<string name="cleanup_residue_description">Bersihkan file dan direktori sisa dari berbagai modul dan alat (dapat menyebabkan penghapusan yang tidak disengaja, kehilangan data, dan gagal boot, gunakan dengan hati-hati)</string>
</resources>

View file

@ -63,7 +63,6 @@
<string name="require_kernel_version" formatted="false">Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manager dengan baik. Harap tingkatkan ke versi %s atau yang lebih tinggi!</string>
<string name="settings_umount_modules_default">Melepas Modul secara bawaan</string>
<string name="settings_umount_modules_default_summary">Menggunakan \"Umount Modul\" secara universal pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil.</string>
<string name="settings_susfs_toggle">Nonaktifkan kprobe hooks</string>
<string name="profile_umount_modules_summary">Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini.</string>
<string name="profile_selinux_domain">Domain</string>
<string name="profile_selinux_rules">Aturan</string>
@ -112,6 +111,8 @@
\nHANYA gunakan setelah proses OTA selesai.
\nLanjutkan?</string>
<string name="install_next">Selanjutnya</string>
<string name="install_upload_lkm_file">Gunakan berkas LKM lokal</string>
<string name="install_only_support_ko_file">Hanya berkas .ko yang didukung</string>
<string name="select_file_tip">%1$s image partisi terekomendasi</string>
<string name="select_file_tip_vendor">(tidak stabil)</string>
<string name="select_kmi">Pilih KMI</string>
@ -166,6 +167,13 @@
<string name="su_not_allowed">Memberikan hak superuser kepada %s tidak diizinkan</string>
<string name="settings_disable_su">Nonaktifkan kompatibilitas SU</string>
<string name="settings_disable_su_summary">Nonaktifkan sementara kemampuan aplikasi untuk mendapatkan hak akses root melalui perintah su (proses root yang sedang berjalan tidak akan terpengaruh)</string>
<string name="settings_disable_kernel_umount">Nonaktifkan pelepasan (unmount) kernel</string>
<string name="settings_disable_kernel_umount_summary">Nonaktifkan perilaku unmount pada level kernel yang digunakan oleh KernelSU.</string>
<string name="settings_enable_enhanced_security">Aktifkan keamanan yang ditingkatkan</string>
<string name="settings_enable_enhanced_security_summary">Aktifkan kebijakan keamanan yang lebih ketat.</string>
<string name="settings_mode_default">Bawaan</string>
<string name="settings_mode_temp_enable">Aktifkan sementara</string>
<string name="settings_mode_always_enable">Aktifkan secara permanen</string>
<string name="module_install_multiple_confirm_with_names">Apakah Anda yakin ingin menginstal %1$d modul berikut?\n\n%2$s</string>
<string name="more_settings">Setelan lainnya</string>
<string name="selinux">Selinux</string>
@ -240,7 +248,6 @@
<string name="invalid_file_type">Format file tidak sesuai. Silakan pilih file dengan format .kpm.</string>
<string name="confirm_uninstall_title_with_filename">Menghapus instalan</string>
<string name="confirm_uninstall_content">KPM berikut akan diuninstall: %s</string>
<string name="settings_susfs_toggle_summary">Nonaktifkan kprobe hooks yang dibuat oleh KernelSU, gunakan inline hooks sebagai gantinya (metode ini mirip dengan hooking kernel non-GKI).</string>
<string name="image_editor_hint">Gunakan dua jari untuk memperbesar gambar, dan satu jari untuk menggeser mengatur posisi</string>
<string name="reprovision">Reprovisi</string>
<!-- Kernel Flash Progress Related -->
@ -314,9 +321,8 @@
<string name="dpi_confirm_summary">Aplikasi membutuhkan restar untuk menerapkan opsi DPI ini, perubahan ini tidak mengganggu DPI sistem</string>
<string name="dpi_applied_success">DPI telah di rubah ke %1$d, efektif setelah aplikasi di restar</string>
<!-- Language settings related strings -->
<string name="language_setting">Bahasa Aplikasi</string>
<string name="language_follow_system">Mengikuti sistem</string>
<string name="language_changed">Bahasa dirubah, mulai ulang aplikasi untuk menerapkan</string>
<string name="settings_language">Bahasa Aplikasi</string>
<string name="language_system_default">Mengikuti sistem</string>
<string name="settings_card_dim">Penyesuaian Kegelapan Kartu</string>
<!-- Flash related -->
<string name="error_code">Kode error</string>
@ -413,8 +419,6 @@
<string name="susfs_try_umount_added_success">Jalur coba umount berhasil ditambahkan %s</string>
<string name="susfs_try_umount_added_saved">Jalur coba umount berhasil disimpan %s</string>
<!-- SuSFS Run Umount -->
<string name="susfs_run_umount_confirm_title">Konfirmasi Jalankan Coba Umount</string>
<string name="susfs_run_umount_confirm_message">Ini akan segera mengeksekusi semua operasi umount yang dikonfigurasi. Apakah Anda yakin ingin melanjutkan?</string>
<!-- SuSFS Reset Categories -->
<string name="susfs_reset_paths_title">Setel Ulang Jalur SUS</string>
<string name="susfs_reset_paths_message">Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan?</string>
@ -445,7 +449,6 @@
<string name="auto_bind_mount_feature_label">Pemasangan Bind Otomatis</string>
<string name="auto_try_umount_bind_feature_label">Coba Umount Bind Mount Otomatis</string>
<string name="hide_symbols_feature_label">Sembunyikan Simbol KSU SUSFS</string>
<string name="magic_mount_feature_label">Dukungan Pemasangan Ajaib</string>
<string name="sus_kstat_feature_label">Dukungan SUS Kstat</string>
<string name="sus_su_feature_label">Fungsi pengalihan mode SUS SU</string>
<!-- 可切换状态 -->
@ -513,7 +516,6 @@
<string name="susfs_hide_mounts_current_setting">Pengaturan saat ini: %s</string>
<string name="susfs_hide_mounts_setting_all">Sembunyikan untuk semua proses</string>
<string name="susfs_hide_mounts_setting_non_ksu">Sembunyikan hanya untuk proses non-KSU</string>
<string name="susfs_run">Jalankan</string>
<string name="kernel_simple_kernel">Mode Ringkas Versi Kernel</string>
<string name="kernel_simple_kernel_summary">Aktifkan atau nonaktifkan mode bersih yang ditampilkan oleh versi kernel SukiSU</string>
<string name="susfs_android_data_path_set">Jalur Data Android telah diatur ke: %s</string>
@ -564,6 +566,8 @@
<string name="add_custom_path">Lainnya</string>
<string name="add_app_path">Aplikasi</string>
<string name="susfs_add_app_path">Tambahkan Jalur Aplikasi</string>
<string name="susfs_version_mismatch">Versi pustaka SuSFS tidak cocok, kernel: %1$s vs manajer: %2$s. Disarankan untuk memperbarui kernel atau manajer</string>
<string name="warning">Peringatan</string>
<string name="search_apps">Cari Aplikasi</string>
<string name="selected_apps_count">%1$d aplikasi dipilih</string>
<string name="already_added_apps_count">%1$d aplikasi sudah ditambahkan</string>
@ -607,6 +611,14 @@
<string name="sus_loop_path_feature_label">Jalur Loop SUS</string>
<string name="sus_loop_paths_description_title">Konfigurasi Jalur Loop</string>
<string name="sus_loop_paths_description_text">Jalur loop ditandai ulang sebagai SUS_PATH pada setiap startup aplikasi pengguna non-root atau layanan terisolasi. Ini membantu mengatasi masalah di mana jalur yang ditambahkan mungkin memiliki status inode direset atau inode dibuat ulang di kernel.</string>
<string name="avc_log_spoofing">Palsukan log AVC</string>
<string name="avc_log_spoofing_enabled">Palsukan log AVC telah diaktifkan</string>
<string name="avc_log_spoofing_disabled">Palsukan log AVC telah dinonaktifkan</string>
<string name="avc_log_spoofing_description">Dinonaktifkan: Nonaktifkan pemalsuan sus tcontext dari \'su\' yang ditampilkan di avc log di kernel\n
Diaktifkan: Aktifkan pemalsuan sus tcontext dari \'su\' dengan \'kernel\' yang ditampilkan di avc log in kernel</string>
<string name="avc_log_spoofing_warning">Catatan Penting:\n
- Secara default pada kernel nilai ini disetel ke \'0\'\n
- Mengaktifkan ini terkadang membuat pengembang lebih sulit mengidentifikasi penyebab saat melakukan debugging terkait izin atau masalah SELinux, sehingga disarankan agar pengguna menonaktifkannya saat sedang melakukan debugging</string>
<!-- 模块签名功能描述 -->
<string name="module_verified">Tervalidasi</string>
<string name="module_signature_verified">Tanda tangan modul tervalidasi</string>
@ -615,4 +627,77 @@
<string name="module_signature_invalid">Penerbit tidak dikenal</string>
<string name="module_signature_invalid_message">Modul yang tidak ditandatangani mungkin tidak lengkap. Untuk melindungi perangkat Anda, pemasangan modul ini diblokir.</string>
<string name="module_signature_verification_failed">Modul yang tidak ditandatangani mungkin tidak lengkap. Apakah Anda ingin mengizinkan modul berikut dari penerbit tidak dikenal untuk dipasang di perangkat ini?</string>
<string name="home_hook_type">Jenis hook</string>
<!-- KPM patching related strings -->
<string name="kpm_patch_options">Patch KPM</string>
<string name="kpm_patch_description">Untuk menambahkan fitur KPM tambahan</string>
<string name="enable_kpm_patch">Patch KPM</string>
<string name="kpm_patch_switch_description">Terapkan patch KPM ke image kernel sebelum melakukan flashing</string>
<string name="enable_kpm_undo_patch">Batalkan Patch KPM</string>
<string name="kpm_undo_patch_switch_description">Batalkan patch KPM yang telah diterapkan sebelumnya</string>
<string name="kpm_patch_enabled">Patch KPM aktif</string>
<string name="kpm_undo_patch_enabled">Pembatalan patch KPM diaktifkan</string>
<string name="kpm_patch_mode">Mode Patch KPM</string>
<string name="kpm_undo_patch_mode">Mode Pembatalan Patch KPM</string>
<!-- KPM workflow related -->
<string name="kpm_preparing_tools">Sedang menyiapkan Alat KPM</string>
<string name="kpm_applying_patch">Menerapkan patch KPM</string>
<string name="kpm_undoing_patch">Membatalkan patch KPM</string>
<string name="kpm_found_image_file">Menemukan berkas Image: %s</string>
<string name="kpm_patch_success">KPM berhasil diterapkan</string>
<string name="kpm_undo_patch_success">Patch KPM berhasil dibatalkan</string>
<string name="kpm_file_repacked">File berhasil direpack</string>
<!-- KPM error messages -->
<string name="kpm_extract_zip_failed">Gagal mengekstrak berkas zip</string>
<string name="kpm_image_file_not_found">Berkas Image tidak ditemukan</string>
<string name="kpm_patch_failed">Patch KPM gagal</string>
<string name="kpm_undo_patch_failed">Pembatalan patch KPM gagal</string>
<string name="kpm_patch_operation_failed">Operasi patch KPM gagal: %s</string>
<!-- KPM option radio group strings -->
<string name="kpm_follow_kernel_file">Ikuti kernel</string>
<string name="kpm_follow_kernel_description">Gunakan kernel apa adanya tanpa perubahan dari KPM</string>
<!-- UID Scanner Settings -->
<string name="uid_auto_scan_title">Daftar aplikasi pemindaian pada mode pengguna</string>
<string name="uid_auto_scan_summary">Mengaktifkan opsi ini akan menggunakan pemindaian mode pengguna untuk daftar aplikasi, sehingga meningkatkan kestabilan. (Jika Anda mengalami masalah seperti hang saat kernel memindai daftar aplikasi, Anda dapat mencoba mengaktifkan opsi ini.)</string>
<string name="uid_multi_user_scan_title">Pemindaian Aplikasi Multi-Pengguna</string>
<string name="uid_multi_user_scan_summary">Ketika diaktifkan, fitur ini akan memindai aplikasi untuk semua pengguna, termasuk profil kerja</string>
<string name="uid_scanner_setting_failed">Gagal mengatur, silakan periksa perizinan</string>
<string name="uid_scanner_setting_error">Gagal mengatur: %s</string>
<string name="clean_runtime_environment">Bersihkan Lingkungan Runtime</string>
<string name="clean_runtime_environment_summary">Bersihkan berkas runtime dan hentikan layanan pemindai</string>
<string name="clean_runtime_environment_confirm">Apakah Anda yakin ingin membersihkan lingkungan runtime? Tindakan ini akan menghentikan layanan pemindai dan menghapus berkas yang terkait.</string>
<string name="clean_runtime_environment_success">Lingkungan runtime berhasil dibersihkan</string>
<string name="clean_runtime_environment_failed">Gagal membersihkan lingkungan runtime</string>
<!-- 确认安装相关字符串 -->
<string name="confirm_installation">Konfirmasi Instalasi</string>
<string name="confirm_multiple_installation">Konfirmasi Instalasi (Berkas %d)</string>
<string name="install_confirm">Instal</string>
<string name="module_package">Modul</string>
<string name="kernel_package">Kernel</string>
<string name="unknown_package">Tidak diketahui</string>
<string name="unknown_kernel">Kernel tidak diketahui</string>
<string name="unknown_file">Berkas tidak diketahui</string>
<string name="version">Versi</string>
<string name="author">Pembuat</string>
<string name="description">Deskripsi</string>
<string name="supported_devices">Perangkat yang didukung</string>
<!-- SUS Map related strings -->
<string name="susfs_tab_sus_maps">Peta SUS</string>
<string name="susfs_sus_map_label">Jalur Pustaka</string>
<string name="susfs_sus_map_placeholder">/data/adb/modules/my_module/zygisk/arm64-v8a.so</string>
<string name="susfs_add_sus_map">Tambahkan Peta SUS</string>
<string name="susfs_edit_sus_map">Sunting Peta SUS</string>
<string name="susfs_sus_map_added_success">Peta SUS berhasil ditambahkan: %1$s</string>
<string name="susfs_sus_map_removed">Peta SUS telah dihapus: %1$s</string>
<string name="susfs_sus_map_updated">Peta SUS telah diperbarui: %1$s -&gt; %2$s</string>
<string name="susfs_no_sus_maps_configured">Tidak ada peta SUS yang dikonfigurasi</string>
<string name="susfs_reset_sus_maps_title">Atur ulang Peta SUS</string>
<string name="susfs_reset_sus_maps_message">Tindakan ini akan menghapus semua peta SUS yang telah dikonfigurasi. Tindakan ini tidak dapat dibatalkan.</string>
<string name="sus_maps_section">Penyembunyian Peta Memori</string>
<string name="sus_maps_description_title">Sembunyikan berkas nyata yang di-mmapped dari berbagai peta di /proc/self/</string>
<!-- Log Viewer -->
<string name="log_viewer_search">Cari</string>
<string name="log_viewer_clear_logs">Bersihkan Log</string>
<string name="log_viewer_clear_logs_confirm">Apakah Anda yakin ingin mengosongkan berkas log yang dipilih? Tindakan ini tidak dapat dibatalkan.</string>
<!-- MiUI Uninstall Desc -->
</resources>

Some files were not shown because too many files have changed in this diff Show more