Updated to 4.0.0
This commit is contained in:
parent
b7554a5383
commit
938198bf11
234 changed files with 21069 additions and 12710 deletions
|
|
@ -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)
|
||||
|
|
|
|||
4
manager/app/proguard-rules.pro
vendored
4
manager/app/proguard-rules.pro
vendored
|
|
@ -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.** { *; }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
10
manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl
Normal file
10
manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl
Normal 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.
Binary file not shown.
Binary file not shown.
BIN
manager/app/src/main/assets/ksu_susfs_2.0.0
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_2.0.0
Normal file
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
163
manager/app/src/main/cpp/legacy.c
Normal file
163
manager/app/src/main/cpp/legacy.c
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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("[H[J")) { // 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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -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(
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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("# 路径配置")
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
13
manager/app/src/main/jniLibs/.gitignore
vendored
13
manager/app/src/main/jniLibs/.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
BIN
manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so
Normal file
BIN
manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
541
manager/app/src/main/res/values-idn/strings.xml
Normal file
541
manager/app/src/main/res/values-idn/strings.xml
Normal 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>
|
||||
|
|
@ -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 -> %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
Loading…
Add table
Add a link
Reference in a new issue