Repo cloned

This commit is contained in:
Fr4nz D13trich 2026-01-04 20:10:16 +01:00
commit 11ea8025b0
214 changed files with 33943 additions and 0 deletions

2
app/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/build
/release/

294
app/build.gradle.kts Normal file
View file

@ -0,0 +1,294 @@
@file:Suppress("UnstableApiUsage")
import com.android.build.gradle.tasks.PackageAndroidArtifact
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.net.URI
plugins {
alias(libs.plugins.agp.app)
alias(libs.plugins.kotlin)
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.ksp)
alias(libs.plugins.lsplugin.apksign)
alias(libs.plugins.lsplugin.resopt)
alias(libs.plugins.lsplugin.cmaker)
id("kotlin-parcelize")
}
val managerVersionCode: Int by rootProject.extra
val managerVersionName: String by rootProject.extra
val branchname: String by rootProject.extra
val kernelPatchVersion: String by rootProject.extra
apksign {
storeFileProperty = "KEYSTORE_FILE"
storePasswordProperty = "KEYSTORE_PASSWORD"
keyAliasProperty = "KEY_ALIAS"
keyPasswordProperty = "KEY_PASSWORD"
}
android {
namespace = "me.bmax.apatch"
buildTypes {
debug {
isDebuggable = true
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
release {
isMinifyEnabled = true
isShrinkResources = true
isDebuggable = false
multiDexEnabled = true
vcsInfo.include = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
dependenciesInfo.includeInApk = false
// https://stackoverflow.com/a/77745844
tasks.withType<PackageAndroidArtifact> {
doFirst { appMetadata.asFile.orNull?.writeText("") }
}
buildFeatures {
aidl = true
buildConfig = true
compose = true
prefab = true
}
defaultConfig {
buildConfigField("String", "buildKPV", "\"$kernelPatchVersion\"")
base.archivesName = "APatch_${managerVersionCode}_${managerVersionName}_on_${branchname}"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
packaging {
jniLibs {
useLegacyPackaging = true
}
resources {
excludes += "**"
merges += "META-INF/com/google/android/**"
}
}
externalNativeBuild {
cmake {
version = "3.28.0+"
path("src/main/cpp/CMakeLists.txt")
}
}
androidResources {
generateLocaleConfig = true
}
sourceSets["main"].jniLibs.srcDir("libs")
applicationVariants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
}
fun registerDownloadTask(
taskName: String, srcUrl: String, destPath: String, project: Project
) {
project.tasks.register(taskName) {
val destFile = File(destPath)
doLast {
if (!destFile.exists() || isFileUpdated(srcUrl, destFile)) {
println(" - Downloading $srcUrl to ${destFile.absolutePath}")
downloadFile(srcUrl, destFile)
println(" - Download completed.")
} else {
println(" - File is up-to-date, skipping download.")
}
}
}
}
fun isFileUpdated(url: String, localFile: File): Boolean {
val connection = URI.create(url).toURL().openConnection()
val remoteLastModified = connection.getHeaderFieldDate("Last-Modified", 0L)
return remoteLastModified > localFile.lastModified()
}
fun downloadFile(url: String, destFile: File) {
URI.create(url).toURL().openStream().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
registerDownloadTask(
taskName = "downloadKpimg",
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kpimg-android",
destPath = "${project.projectDir}/src/main/assets/kpimg",
project = project
)
registerDownloadTask(
taskName = "downloadKptools",
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kptools-android",
destPath = "${project.projectDir}/libs/arm64-v8a/libkptools.so",
project = project
)
// Compat kp version less than 0.10.7
// TODO: Remove in future
registerDownloadTask(
taskName = "downloadCompatKpatch",
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/0.10.7/kpatch-android",
destPath = "${project.projectDir}/libs/arm64-v8a/libkpatch.so",
project = project
)
tasks.register<Copy>("mergeScripts") {
into("${project.projectDir}/src/main/resources/META-INF/com/google/android")
from(rootProject.file("${project.rootDir}/scripts/update_binary.sh")) {
rename { "update-binary" }
}
from(rootProject.file("${project.rootDir}/scripts/update_script.sh")) {
rename { "updater-script" }
}
}
tasks.getByName("preBuild").dependsOn(
"downloadKpimg",
"downloadKptools",
"downloadCompatKpatch",
"mergeScripts",
)
// https://github.com/bbqsrc/cargo-ndk
// cargo ndk -t arm64-v8a build --release
tasks.register<Exec>("cargoBuild") {
executable("cargo")
args("ndk", "-t", "arm64-v8a", "build", "--release")
workingDir("${project.rootDir}/apd")
}
tasks.register<Copy>("buildApd") {
dependsOn("cargoBuild")
from("${project.rootDir}/apd/target/aarch64-linux-android/release/apd")
into("${project.projectDir}/libs/arm64-v8a")
rename("apd", "libapd.so")
}
tasks.configureEach {
if (name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders") {
dependsOn("buildApd")
}
}
tasks.register<Exec>("cargoClean") {
executable("cargo")
args("clean")
workingDir("${project.rootDir}/apd")
}
tasks.register<Delete>("apdClean") {
dependsOn("cargoClean")
delete(file("${project.projectDir}/libs/arm64-v8a/libapd.so"))
}
tasks.clean {
dependsOn("apdClean")
}
ksp {
arg("compose-destinations.defaultTransitions", "none")
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.webkit)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.runtime.livedata)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp)
implementation(libs.com.github.topjohnwu.libsu.core)
implementation(libs.com.github.topjohnwu.libsu.service)
implementation(libs.com.github.topjohnwu.libsu.nio)
implementation(libs.com.github.topjohnwu.libsu.io)
implementation(libs.dev.rikka.rikkax.parcelablelist)
implementation(libs.io.coil.kt.coil.compose)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.me.zhanghai.android.appiconloader.coil)
implementation(libs.sheet.compose.dialogs.core)
implementation(libs.sheet.compose.dialogs.list)
implementation(libs.sheet.compose.dialogs.input)
implementation(libs.markdown)
implementation(libs.ini4j)
compileOnly(libs.cxx)
}
cmaker {
default {
arguments += "-DANDROID_STL=none"
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
abiFilters("arm64-v8a")
cppFlags += "-std=c++2b"
cFlags += "-std=c2x"
}
}

4
app/libs/arm64-v8a/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
libkptools.so
libapjni.so
libkpatch.so
libapd.so

BIN
app/libs/arm64-v8a/libbootctl.so Executable file

Binary file not shown.

BIN
app/libs/arm64-v8a/libbusybox.so Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

26
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,26 @@
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-dontwarn java.beans.Introspector
-dontwarn java.beans.VetoableChangeListener
-dontwarn java.beans.VetoableChangeSupport
# Keep ini4j Service Provider Interface
-keep,allowobfuscation,allowoptimization public class org.ini4j.spi.*
# Kotlin
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void check*(...);
public static void throw*(...);
}
-repackageclasses
-allowaccessmodification
-overloadaggressively
-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".APApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="APatch"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.APatch"
android:usesCleartextTraffic="true"
tools:targetApi="34">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.WebUIActivity"
android:autoRemoveFromRecents="true"
android:documentLaunchMode="intoExisting"
android:exported="false"
android:theme="@style/Theme.APatch.WebUI" />
<activity
android:name=".ui.CrashHandleActivity"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
</manifest>

View file

@ -0,0 +1,9 @@
// IAPRootService.aidl
package me.bmax.apatch;
import android.content.pm.PackageInfo;
import rikka.parcelablelist.ParcelableListSlice;
interface IAPRootService {
ParcelableListSlice<PackageInfo> getPackages(int flags);
}

2
app/src/main/assets/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
kpimg
*.kpm

View file

@ -0,0 +1,116 @@
#!/bin/sh
# By SakuraKyuo
OUTFD=/proc/self/fd/$2
function ui_print() {
echo -e "ui_print $1\nui_print" >> $OUTFD
}
function ui_printfile() {
while IFS='' read -r line || $BB [[ -n "$line" ]]; do
ui_print "$line";
done < $1;
}
function kernelFlagsErr(){
ui_print "- Installation has Aborted!"
ui_print "- APatch requires CONFIG_KALLSYMS to be Enabled."
ui_print "- But your kernel seems NOT enabled it."
exit
}
function apatchNote(){
ui_print "- APatch Patch Done"
ui_print "- APatch Key is: Ap$skey"
ui_print "- We do have saved Origin Boot image to /data"
ui_print "- If you encounter bootloop, reboot into Recovery and flash it"
exit
}
function failed(){
ui_printfile /dev/tmp/install/log
ui_print "- APatch Patch Failed."
ui_print "- Please feedback to the developer with the screenshots."
exit
}
function boot_execute_ab(){
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then
kernelFlagsErr
fi
mv kernel kernel-origin
./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log
if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then
failed
fi
ui_printfile /dev/tmp/install/log
./lib/arm64-v8a/libmagiskboot.so repack boot.img
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
mv boot.img /data/boot.img
apatchNote
}
function boot_execute(){
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then
kernelFlagsErr
fi
mv kernel kernel-origin
./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log
if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then
failed
fi
ui_printfile /dev/tmp/install/log
./lib/arm64-v8a/libmagiskboot.so repack boot.img
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
mv boot.img /data/boot.img
apatchNote
}
function main(){
cd /dev/tmp/install
chmod a+x ./assets/kpimg
chmod a+x ./lib/arm64-v8a/libkptools.so
chmod a+x ./lib/arm64-v8a/libmagiskboot.so
slot=$(getprop ro.boot.slot_suffix)
skey=$(cat /proc/sys/kernel/random/uuid | cut -d \- -f1)
if [[ ! "$slot" == "" ]]; then
ui_print ""
ui_print "- You are using A/B device."
# Script author
ui_print "- Install Script by SakuraKyuo"
# Get kernel
ui_print ""
dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img
if [[ "$?" == 0 ]]; then
ui_print "- Detected boot partition."
boot_execute_ab
fi
else
ui_print "You are using A Only device."
# Get kernel
ui_print ""
dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img
if [[ "$?" == 0 ]]; then
ui_print "- Detected boot partition."
boot_execute
fi
fi
}
main

View file

@ -0,0 +1,89 @@
#!/bin/sh
# By SakuraKyuo
OUTFD=/proc/self/fd/$2
function ui_print() {
echo -e "ui_print $1\nui_print" >> $OUTFD
}
function ui_printfile() {
while IFS='' read -r line || $BB [[ -n "$line" ]]; do
ui_print "$line";
done < $1;
}
function apatchNote(){
ui_print "- APatch Unpatch Done"
exit
}
function failed(){
ui_print "- APatch Unpatch Failed."
ui_print "- Please feedback to the developer with the screenshots."
exit
}
function boot_execute_ab(){
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
mv kernel kernel-origin
./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel
if [[ ! "$?" == 0 ]]; then
failed
fi
./lib/arm64-v8a/libmagiskboot.so repack boot.img
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
apatchNote
}
function boot_execute(){
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
mv kernel kernel-origin
./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel
if [[ ! "$?" == 0 ]]; then
failed
fi
./lib/arm64-v8a/libmagiskboot.so repack boot.img
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot
apatchNote
}
function main(){
cd /dev/tmp/install
chmod a+x ./lib/arm64-v8a/libkptools.so
chmod a+x ./lib/arm64-v8a/libmagiskboot.so
slot=$(getprop ro.boot.slot_suffix)
if [[ ! "$slot" == "" ]]; then
ui_print ""
ui_print "- You are using A/B device."
# Get kernel
ui_print ""
dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img
if [[ "$?" == 0 ]]; then
ui_print "- Detected boot partition."
boot_execute_ab
fi
else
ui_print "You are using A Only device."
# Get kernel
ui_print ""
dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img
if [[ "$?" == 0 ]]; then
ui_print "- Detected boot partition."
boot_execute
fi
fi
}
main

View file

@ -0,0 +1,20 @@
#!/system/bin/sh
ARCH=$(getprop ro.product.cpu.abi)
IS_INSTALL_NEXT_SLOT=$1
# Load utility functions
. ./util_functions.sh
if [ "$IS_INSTALL_NEXT_SLOT" = "true" ]; then
get_next_slot
else
get_current_slot
fi
find_boot_image
[ -e "$BOOTIMAGE" ] || { >&2 echo "- can't find boot.img!"; exit 1; }
true

View file

@ -0,0 +1,107 @@
#!/system/bin/sh
#######################################################################################
# APatch Boot Image Patcher
#######################################################################################
#
# Usage: boot_patch.sh <superkey> <bootimage> [ARGS_PASS_TO_KPTOOLS]
#
# This script should be placed in a directory with the following files:
#
# File name Type Description
#
# boot_patch.sh script A script to patch boot image for APatch.
# (this file) The script will use files in its same
# directory to complete the patching process.
# bootimg binary The target boot image
# kpimg binary KernelPatch core Image
# kptools executable The KernelPatch tools binary to inject kpimg to kernel Image
# magiskboot executable Magisk tool to unpack boot.img.
#
#######################################################################################
ARCH=$(getprop ro.product.cpu.abi)
# Load utility functions
. ./util_functions.sh
echo "****************************"
echo " APatch Boot Image Patcher"
echo "****************************"
SUPERKEY="$1"
BOOTIMAGE=$2
FLASH_TO_DEVICE=$3
shift 2
[ -z "$SUPERKEY" ] && { >&2 echo "- SuperKey empty!"; exit 1; }
[ -e "$BOOTIMAGE" ] || { >&2 echo "- $BOOTIMAGE does not exist!"; exit 1; }
# Check for dependencies
command -v ./magiskboot >/dev/null 2>&1 || { >&2 echo "- Command magiskboot not found!"; exit 1; }
command -v ./kptools >/dev/null 2>&1 || { >&2 echo "- Command kptools not found!"; exit 1; }
if [ ! -f kernel ]; then
echo "- Unpacking boot image"
./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1
if [ $? -ne 0 ]; then
>&2 echo "- Unpack error: $?"
exit $?
fi
fi
if [ ! $(./kptools -i kernel -f | grep CONFIG_KALLSYMS=y) ]; then
echo "- Patcher has Aborted!"
echo "- APatch requires CONFIG_KALLSYMS to be Enabled."
echo "- But your kernel seems NOT enabled it."
exit 0
fi
if [ $(./kptools -i kernel -l | grep patched=false) ]; then
echo "- Backing boot.img "
cp "$BOOTIMAGE" "ori.img" >/dev/null 2>&1
fi
mv kernel kernel.ori
echo "- Patching kernel"
set -x
./kptools -p -i kernel.ori -S "$SUPERKEY" -k kpimg -o kernel "$@"
patch_rc=$?
set +x
if [ $patch_rc -ne 0 ]; then
>&2 echo "- Patch kernel error: $patch_rc"
exit $?
fi
echo "- Repacking boot image"
./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1
if [ ! $(./kptools -i kernel.ori -f | grep CONFIG_KALLSYMS_ALL=y) ]; then
echo "- Detected CONFIG_KALLSYMS_ALL is not set!"
echo "- APatch has patched but maybe your device won't boot."
echo "- Make sure you have original boot image backup."
fi
if [ $? -ne 0 ]; then
>&2 echo "- Repack error: $?"
exit $?
fi
if [ "$FLASH_TO_DEVICE" = "true" ]; then
# flash
if [ -b "$BOOTIMAGE" ] || [ -c "$BOOTIMAGE" ] && [ -f "new-boot.img" ]; then
echo "- Flashing new boot image"
flash_image new-boot.img "$BOOTIMAGE"
if [ $? -ne 0 ]; then
>&2 echo "- Flash error: $?"
exit $?
fi
fi
echo "- Successfully Flashed!"
else
echo "- Successfully Patched!"
fi

View file

@ -0,0 +1,74 @@
#!/system/bin/sh
#######################################################################################
# APatch Boot Image Unpatcher
#######################################################################################
ARCH=$(getprop ro.product.cpu.abi)
# Load utility functions
. ./util_functions.sh
echo "****************************"
echo " APatch Boot Image Unpatcher"
echo "****************************"
BOOTIMAGE=$1
[ -e "$BOOTIMAGE" ] || { echo "- $BOOTIMAGE does not exist!"; exit 1; }
echo "- Target image: $BOOTIMAGE"
# Check for dependencies
command -v ./magiskboot >/dev/null 2>&1 || { echo "- Command magiskboot not found!"; exit 1; }
command -v ./kptools >/dev/null 2>&1 || { echo "- Command kptools not found!"; exit 1; }
if [ ! -f kernel ]; then
echo "- Unpacking boot image"
./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1
if [ $? -ne 0 ]; then
>&2 echo "- Unpack error: $?"
exit $?
fi
fi
if [ ! $(./kptools -i kernel -l | grep patched=false) ]; then
echo "- kernel has been patched "
if [ -f "new-boot.img" ]; then
echo "- found backup boot.img ,use it for recovery"
else
mv kernel kernel.ori
echo "- Unpatching kernel"
./kptools -u --image kernel.ori --out kernel
if [ $? -ne 0 ]; then
>&2 echo "- Unpatch error: $?"
exit $?
fi
echo "- Repacking boot image"
./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1
if [ $? -ne 0 ]; then
>&2 echo "- Repack error: $?"
exit $?
fi
fi
else
echo "- no need unpatch"
exit 0
fi
if [ -f "new-boot.img" ]; then
echo "- Flashing boot image"
flash_image new-boot.img "$BOOTIMAGE"
if [ $? -ne 0 ]; then
>&2 echo "- Flash error: $?"
exit $?
fi
fi
echo "- Flash successful"
# Reset any error code
true

View file

@ -0,0 +1,537 @@
#!/system/bin/sh
#######################################################################################
# Helper Functions (credits to topjohnwu)
#######################################################################################
APATCH_VER='0.10.4'
APATCH_VER_CODE=164
ui_print() {
if $BOOTMODE; then
echo "$1"
else
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
fi
}
toupper() {
echo "$@" | tr '[:lower:]' '[:upper:]'
}
grep_cmdline() {
local REGEX="s/^$1=//p"
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
} 2>/dev/null | sed -n "$REGEX"
}
grep_prop() {
local REGEX="s/^$1=//p"
shift
local FILES=$@
[ -z "$FILES" ] && FILES='/system/build.prop'
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1
}
getvar() {
local VARNAME=$1
local VALUE
local PROPPATH='/data/.magisk /cache/.magisk'
[ ! -z $MAGISKTMP ] && PROPPATH="$MAGISKTMP/.magisk/config $PROPPATH"
VALUE=$(grep_prop $VARNAME $PROPPATH)
[ ! -z $VALUE ] && eval $VARNAME=\$VALUE
}
is_mounted() {
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
return $?
}
abort() {
ui_print "$1"
$BOOTMODE || recovery_cleanup
[ ! -z $MODPATH ] && rm -rf $MODPATH
rm -rf $TMPDIR
exit 1
}
set_nvbase() {
NVBASE="$1"
MAGISKBIN="$1/magisk"
}
print_title() {
local len line1len line2len bar
line1len=$(echo -n $1 | wc -c)
line2len=$(echo -n $2 | wc -c)
len=$line2len
[ $line1len -gt $line2len ] && len=$line1len
len=$((len + 2))
bar=$(printf "%${len}s" | tr ' ' '*')
ui_print "$bar"
ui_print " $1 "
[ "$2" ] && ui_print " $2 "
ui_print "$bar"
}
setup_flashable() {
ensure_bb
$BOOTMODE && return
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
# We will have to manually find out OUTFD
for FD in $(ls /proc/$$/fd); do
if readlink /proc/$$/fd/$FD | grep -q pipe; then
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
OUTFD=$FD
break
fi
fi
done
fi
recovery_actions
}
ensure_bb() {
if set -o | grep -q standalone; then
# We are definitely in busybox ash
set -o standalone
return
fi
# Find our busybox binary
local bb
if [ -f $TMPDIR/busybox ]; then
bb=$TMPDIR/busybox
elif [ -f $MAGISKBIN/busybox ]; then
bb=$MAGISKBIN/busybox
else
abort "! Cannot find BusyBox"
fi
chmod 755 $bb
# Busybox could be a script, make sure /system/bin/sh exists
if [ ! -f /system/bin/sh ]; then
umount -l /system 2>/dev/null
mkdir -p /system/bin
ln -s $(command -v sh) /system/bin/sh
fi
export ASH_STANDALONE=1
# Find our current arguments
# Run in busybox environment to ensure consistent results
# /proc/<pid>/cmdline shall be <interpreter> <script> <arguments...>
local cmds="$($bb sh -c "
for arg in \$(tr '\0' '\n' < /proc/$$/cmdline); do
if [ -z \"\$cmds\" ]; then
# Skip the first argument as we want to change the interpreter
cmds=\"sh\"
else
cmds=\"\$cmds '\$arg'\"
fi
done
echo \$cmds")"
# Re-exec our script
echo $cmds | $bb xargs $bb
exit
}
recovery_actions() {
# Make sure random won't get blocked
mount -o bind /dev/urandom /dev/random
# Unset library paths
OLD_LD_LIB=$LD_LIBRARY_PATH
OLD_LD_PRE=$LD_PRELOAD
OLD_LD_CFG=$LD_CONFIG_FILE
unset LD_LIBRARY_PATH
unset LD_PRELOAD
unset LD_CONFIG_FILE
}
recovery_cleanup() {
local DIR
ui_print "- Unmounting partitions"
(
if [ ! -d /postinstall/tmp ]; then
umount -l /system
umount -l /system_root
fi
umount -l /vendor
umount -l /persist
umount -l /metadata
for DIR in /apex /system /system_root; do
if [ -L "${DIR}_link" ]; then
rmdir $DIR
mv -f ${DIR}_link $DIR
fi
done
umount -l /dev/random
) 2>/dev/null
[ -z $OLD_LD_LIB ] || export LD_LIBRARY_PATH=$OLD_LD_LIB
[ -z $OLD_LD_PRE ] || export LD_PRELOAD=$OLD_LD_PRE
[ -z $OLD_LD_CFG ] || export LD_CONFIG_FILE=$OLD_LD_CFG
}
find_block() {
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
for BLOCK in "$@"; do
DEVICE=$(find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1) 2>/dev/null
if [ ! -z $DEVICE ]; then
readlink -f $DEVICE
return 0
fi
done
# Fallback by parsing sysfs uevents
for UEVENT in /sys/dev/block/*/uevent; do
DEVNAME=$(grep_prop DEVNAME $UEVENT)
PARTNAME=$(grep_prop PARTNAME $UEVENT)
for BLOCK in "$@"; do
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
echo /dev/block/$DEVNAME
return 0
fi
done
done
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
for DEV in "$@"; do
DEVICE=$(find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1) 2>/dev/null
if [ ! -z $DEVICE ]; then
readlink -f $DEVICE
return 0
fi
done
return 1
}
# After calling this method, the following variables will be set:
# SLOT
get_current_slot() {
# Check A/B slot
SLOT=$(grep_cmdline androidboot.slot_suffix)
if [ -z $SLOT ]; then
SLOT=$(grep_cmdline androidboot.slot)
[ -z $SLOT ] || SLOT=_${SLOT}
fi
if [ -z $SLOT ]; then
SLOT=$(getprop ro.boot.slot_suffix)
fi
[ "$SLOT" = "normal" ] && unset SLOT
[ -z $SLOT ] || echo "SLOT=$SLOT"
}
# After calling this method, the following variables will be set:
# SLOT
# This is used after OTA
get_next_slot() {
# Check A/B slot
SLOT=$(grep_cmdline androidboot.slot_suffix)
if [ -z $SLOT ]; then
SLOT=$(grep_cmdline androidboot.slot)
[ -z $SLOT ] || SLOT=_${SLOT}
fi
if [ -z $SLOT ]; then
SLOT=$(getprop ro.boot.slot_suffix)
fi
[ -z $SLOT ] && { >&2 echo "can't determined next boot slot! check your devices is A/B"; exit 1; }
[ "$SLOT" = "normal" ] && { >&2 echo "can't determined next boot slot! check your devices is A/B"; exit 1; }
if [[ $SLOT == *_a ]]; then
SLOT='_b'
else
SLOT='_a'
fi
echo "SLOT=$SLOT"
}
find_boot_image() {
if [ ! -z $SLOT ]; then
BOOTIMAGE=$(find_block "boot$SLOT")
fi
if [ -z $BOOTIMAGE ]; then
BOOTIMAGE=$(find_block kern-a android_boot kernel bootimg boot lnx boot_a)
fi
if [ -z $BOOTIMAGE ]; then
# Lets see what fstabs tells me
BOOTIMAGE=$(grep -v '#' /etc/*fstab* | grep -E '/boot(img)?[^a-zA-Z]' | grep -oE '/dev/[a-zA-Z0-9_./-]*' | head -n 1)
fi
[ -z $BOOTIMAGE ] || echo "BOOTIMAGE=$BOOTIMAGE"
}
flash_image() {
local CMD1
case "$1" in
*.gz) CMD1="gzip -d < '$1' 2>/dev/null";;
*) CMD1="cat '$1'";;
esac
if [ -b "$2" ]; then {
local img_sz=$(stat -c '%s' "$1")
local blk_sz=$(blockdev --getsize64 "$2")
local blk_bs=$(blockdev --getbsz "$2")
[ "$img_sz" -gt "$blk_sz" ] && return 1
blockdev --setrw "$2"
local blk_ro=$(blockdev --getro "$2")
[ "$blk_ro" -eq 1 ] && return 2
eval "$CMD1" | dd of="$2" bs="$blk_bs" iflag=fullblock conv=notrunc,fsync 2>/dev/null
sync
} elif [ -c "$2" ]; then {
flash_eraseall "$2" >&2
eval "$CMD1" | nandwrite -p "$2" - >&2
} else {
echo "- Not block or char device, storing image"
eval "$CMD1" > "$2" 2>/dev/null
} fi
return 0
}
setup_mntpoint() {
local POINT=$1
[ -L $POINT ] && mv -f $POINT ${POINT}_link
if [ ! -d $POINT ]; then
rm -f $POINT
mkdir -p $POINT
fi
}
mount_name() {
local PART=$1
local POINT=$2
local FLAG=$3
setup_mntpoint $POINT
is_mounted $POINT && return
# First try mounting with fstab
mount $FLAG $POINT 2>/dev/null
if ! is_mounted $POINT; then
local BLOCK=$(find_block $PART)
mount $FLAG $BLOCK $POINT || return
fi
ui_print "- Mounting $POINT"
}
mount_ro_ensure() {
# We handle ro partitions only in recovery
$BOOTMODE && return
local PART=$1
local POINT=$2
mount_name "$PART" $POINT '-o ro'
is_mounted $POINT || abort "! Cannot mount $POINT"
}
# After calling this method, the following variables will be set:
# SLOT, SYSTEM_AS_ROOT, LEGACYSAR
mount_partitions() {
# Check A/B slot
SLOT=$(grep_cmdline androidboot.slot_suffix)
if [ -z $SLOT ]; then
SLOT=$(grep_cmdline androidboot.slot)
[ -z $SLOT ] || SLOT=_${SLOT}
fi
[ "$SLOT" = "normal" ] && unset SLOT
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
# Mount ro partitions
if is_mounted /system_root; then
umount /system 2>/dev/null
umount /system_root 2>/dev/null
fi
mount_ro_ensure "system$SLOT app$SLOT" /system
if [ -f /system/init -o -L /system/init ]; then
SYSTEM_AS_ROOT=true
setup_mntpoint /system_root
if ! mount --move /system /system_root; then
umount /system
umount -l /system 2>/dev/null
mount_ro_ensure "system$SLOT app$SLOT" /system_root
fi
mount -o bind /system_root/system /system
else
if grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts; then
SYSTEM_AS_ROOT=true
else
SYSTEM_AS_ROOT=false
fi
fi
$SYSTEM_AS_ROOT && ui_print "- Device is system-as-root"
LEGACYSAR=false
if $BOOTMODE; then
grep ' / ' /proc/mounts | grep -q '/dev/root' && LEGACYSAR=true
else
# Recovery mode, assume devices that don't use dynamic partitions are legacy SAR
local IS_DYNAMIC=false
if grep -q 'androidboot.super_partition' /proc/cmdline; then
IS_DYNAMIC=true
elif [ -n "$(find_block super)" ]; then
IS_DYNAMIC=true
fi
if $SYSTEM_AS_ROOT && ! $IS_DYNAMIC; then
LEGACYSAR=true
ui_print "- Legacy SAR, force kernel to load rootfs"
fi
fi
}
get_flags() {
if grep ' /data ' /proc/mounts | grep -q 'dm-'; then
ISENCRYPTED=true
elif [ "$(getprop ro.crypto.state)" = "encrypted" ]; then
ISENCRYPTED=true
elif [ "$DATA" = "false" ]; then
# No data access means unable to decrypt in recovery
ISENCRYPTED=true
else
ISENCRYPTED=false
fi
if [ -n "$(find_block vbmeta vbmeta_a)" ]; then
PATCHVBMETAFLAG=false
else
PATCHVBMETAFLAG=true
ui_print "- No vbmeta partition, patch vbmeta in boot image"
fi
# Overridable config flags with safe defaults
getvar KEEPVERITY
getvar KEEPFORCEENCRYPT
getvar RECOVERYMODE
if [ -z $KEEPVERITY ]; then
if $SYSTEM_AS_ROOT; then
KEEPVERITY=true
ui_print "- System-as-root, keep dm-verity"
else
KEEPVERITY=false
fi
fi
if [ -z $KEEPFORCEENCRYPT ]; then
if $ISENCRYPTED; then
KEEPFORCEENCRYPT=true
ui_print "- Encrypted data, keep forceencrypt"
else
KEEPFORCEENCRYPT=false
fi
fi
[ -z $RECOVERYMODE ] && RECOVERYMODE=false
}
install_apatch() {
cd $MAGISKBIN
# Source the boot patcher
SOURCEDMODE=true
. ./boot_patch.sh "$BOOTIMAGE"
ui_print "- Flashing new boot image"
flash_image new-boot.img "$BOOTIMAGE"
case $? in
1)
abort "! Insufficient partition size"
;;
2)
abort "! $BOOTIMAGE is read only"
;;
esac
./magiskboot cleanup
rm -f new-boot.img
run_migrations
}
check_data() {
DATA=false
DATA_DE=false
if grep ' /data ' /proc/mounts | grep -vq 'tmpfs'; then
# Test if data is writable
touch /data/.rw && rm /data/.rw && DATA=true
# Test if data is decrypted
$DATA && [ -d /data/adb ] && touch /data/adb/.rw && rm /data/adb/.rw && DATA_DE=true
$DATA_DE && [ -d /data/adb/magisk ] || mkdir /data/adb/magisk || DATA_DE=false
fi
set_nvbase "/data"
$DATA || set_nvbase "/cache/data_adb"
$DATA_DE && set_nvbase "/data/adb"
}
api_level_arch_detect() {
API=$(grep_get_prop ro.build.version.sdk)
ABI=$(grep_get_prop ro.product.cpu.abi)
if [ "$ABI" = "x86" ]; then
ARCH=x86
ABI32=x86
IS64BIT=false
elif [ "$ABI" = "arm64-v8a" ]; then
ARCH=arm64
ABI32=armeabi-v7a
IS64BIT=true
elif [ "$ABI" = "x86_64" ]; then
ARCH=x64
ABI32=x86
IS64BIT=true
else
ARCH=arm
ABI=armeabi-v7a
ABI32=armeabi-v7a
IS64BIT=false
fi
}
remove_system_su() {
[ -d /postinstall/tmp ] && POSTINST=/postinstall
cd $POSTINST/system
if [ -f bin/su -o -f xbin/su ] && [ ! -f /su/bin/su ]; then
ui_print "- Removing system installed root"
blockdev --setrw /dev/block/mapper/system$SLOT 2>/dev/null
mount -o rw,remount $POSTINST/system
# SuperSU
cd bin
if [ -e .ext/.su ]; then
mv -f app_process32_original app_process32 2>/dev/null
mv -f app_process64_original app_process64 2>/dev/null
mv -f install-recovery_original.sh install-recovery.sh 2>/dev/null
if [ -e app_process64 ]; then
ln -sf app_process64 app_process
elif [ -e app_process32 ]; then
ln -sf app_process32 app_process
fi
fi
# More SuperSU, SuperUser & ROM su
cd ..
rm -rf .pin bin/.ext etc/.installed_su_daemon etc/.has_su_daemon \
xbin/daemonsu xbin/su xbin/sugote xbin/sugote-mksh xbin/supolicy \
bin/app_process_init bin/su /cache/su lib/libsupol.so lib64/libsupol.so \
su.d etc/init.d/99SuperSUDaemon etc/install-recovery.sh /cache/install-recovery.sh \
.supersu /cache/.supersu /data/.supersu \
app/Superuser.apk app/SuperSU /cache/Superuser.apk
elif [ -f /cache/su.img -o -f /data/su.img -o -d /data/su -o -d /data/adb/su ]; then
ui_print "- Removing systemless installed root"
umount -l /su 2>/dev/null
rm -rf /cache/su.img /data/su.img /data/su /data/adb/su /data/adb/suhide \
/cache/.supersu /data/.supersu /cache/supersu_install /data/supersu_install
fi
cd $TMPDIR
}
run_migrations() {
local LOCSHA1
local TARGET
# Legacy app installation
local BACKUP=$MAGISKBIN/stock_boot*.gz
if [ -f $BACKUP ]; then
cp $BACKUP /data
rm -f $BACKUP
fi
# Legacy backup
for gz in /data/stock_boot*.gz; do
[ -f $gz ] || break
LOCSHA1=$(basename $gz | sed -e 's/stock_boot_//' -e 's/.img.gz//')
[ -z $LOCSHA1 ] && break
mkdir /data/magisk_backup_${LOCSHA1} 2>/dev/null
mv $gz /data/magisk_backup_${LOCSHA1}/boot.img.gz
done
# Stock backups
LOCSHA1=$SHA1
for name in boot dtb dtbo dtbs; do
BACKUP=$MAGISKBIN/stock_${name}.img
[ -f $BACKUP ] || continue
if [ $name = 'boot' ]; then
LOCSHA1=$($MAGISKBIN/magiskboot sha1 $BACKUP)
mkdir /data/magisk_backup_${LOCSHA1} 2>/dev/null
fi
TARGET=/data/magisk_backup_${LOCSHA1}/${name}.img
cp $BACKUP $TARGET
rm -f $BACKUP
gzip -9f $TARGET
done
}

View file

@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.28.0)
project("apjni")
find_program(CCACHE ccache)
if (CCACHE)
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE})
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE})
endif ()
find_package(cxx REQUIRED CONFIG)
link_libraries(cxx::cxx)
macro(SET_OPTION option value)
set(${option} ${value} CACHE INTERNAL "" FORCE)
endmacro()
set(CHERISH_POLLY_FLAGS "-mllvm -polly -mllvm -polly-run-dce -mllvm -polly-run-inliner -mllvm -polly-reschedule=1 -mllvm -polly-loopfusion-greedy=1 -mllvm -polly-postopts=1 -mllvm -polly-num-threads=0 -mllvm -polly-omp-backend=LLVM -mllvm -polly-scheduling=dynamic -mllvm -polly-scheduling-chunksize=1 -mllvm -polly-isl-arg=--no-schedule-serialize-sccs -mllvm -polly-ast-use-context -mllvm -polly-detect-keep-going -mllvm -polly-position=before-vectorizer -mllvm -polly-vectorizer=stripmine -mllvm -polly-detect-profitability-min-per-loop-insts=40 -mllvm -polly-invariant-load-hoisting")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CHERISH_POLLY_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -O3 -flto -Wno-vla-cxx-extension")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CHERISH_POLLY_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -fno-rtti -fno-exceptions -O3 -flto -Wno-vla-cxx-extension")
set(CMAKE_CXX_STANDARD 23)
add_library(${PROJECT_NAME} SHARED apjni.cpp)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_STRIP} --strip-all --remove-section=.note.gnu.build-id --remove-section=.note.android.ident $<TARGET_FILE:${PROJECT_NAME}>)
target_link_libraries(${PROJECT_NAME} PRIVATE log)
target_compile_options(${PROJECT_NAME} PRIVATE -flto)
target_link_options(${PROJECT_NAME} PRIVATE "-Wl,--build-id=none" "-Wl,-icf=all,--lto-O3" "-Wl,-s,-x,--gc-sections" "-Wl,--no-undefined")

294
app/src/main/cpp/apjni.cpp Normal file
View file

@ -0,0 +1,294 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2023 bmax121. All Rights Reserved.
* Copyright (C) 2024 GarfieldHan. All Rights Reserved.
* Copyright (C) 2024 1f2003d5. All Rights Reserved.
*/
#include <cstring>
#include <vector>
#include "apjni.hpp"
#include "supercall.h"
jboolean nativeReady(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
return sc_ready(super_key.get());
}
jlong nativeKernelPatchVersion(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
return sc_kp_ver(super_key.get());
}
jstring nativeKernelPatchBuildTime(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
char buf[4096] = { '\0' };
sc_get_build_time(super_key.get(), buf, sizeof(buf));
return env->NewStringUTF(buf);
}
jlong nativeSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint to_uid, jstring selinux_context_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const char *selinux_context = nullptr;
if (selinux_context_jstr) selinux_context = JUTFString(env, selinux_context_jstr);
struct su_profile profile{};
profile.uid = getuid();
profile.to_uid = (uid_t)to_uid;
if (selinux_context) strncpy(profile.scontext, selinux_context, sizeof(profile.scontext) - 1);
long rc = sc_su(super_key.get(), &profile);
if (rc < 0) [[unlikely]] {
LOGE("nativeSu error: %ld", rc);
}
return rc;
}
jint nativeSetUidExclude(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid, jint exclude) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
return static_cast<int>(sc_set_ap_mod_exclude(super_key.get(), (uid_t) uid, exclude));
}
jint nativeGetUidExclude(JNIEnv *env, jobject /* this */, jstring super_key_jstr, uid_t uid) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
return static_cast<int>(sc_get_ap_mod_exclude(super_key.get(), uid));
}
jintArray nativeSuUids(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
int num = static_cast<int>(sc_su_uid_nums(super_key.get()));
if (num <= 0) [[unlikely]] {
LOGW("SuperUser Count less than 1, skip allocating vector...");
return env->NewIntArray(0);
}
std::vector<int> uids(num);
long n = sc_su_allow_uids(super_key.get(), (uid_t *) uids.data(), num);
if (n > 0) [[unlikely]] {
auto array = env->NewIntArray(n);
env->SetIntArrayRegion(array, 0, n, uids.data());
return array;
}
return env->NewIntArray(0);
}
jobject nativeSuProfile(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
struct su_profile profile{};
long rc = sc_su_uid_profile(super_key.get(), (uid_t) uid, &profile);
if (rc < 0) [[unlikely]] {
LOGE("nativeSuProfile error: %ld\n", rc);
return nullptr;
}
jclass cls = env->FindClass("me/bmax/apatch/Natives$Profile");
jmethodID constructor = env->GetMethodID(cls, "<init>", "()V");
jfieldID uidField = env->GetFieldID(cls, "uid", "I");
jfieldID toUidField = env->GetFieldID(cls, "toUid", "I");
jfieldID scontextFild = env->GetFieldID(cls, "scontext", "Ljava/lang/String;");
jobject obj = env->NewObject(cls, constructor);
env->SetIntField(obj, uidField, (int) profile.uid);
env->SetIntField(obj, toUidField, (int) profile.to_uid);
env->SetObjectField(obj, scontextFild, env->NewStringUTF(profile.scontext));
return obj;
}
jlong nativeLoadKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_path_jstr, jstring args_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto module_path = JUTFString(env, module_path_jstr);
const auto args = JUTFString(env, args_jstr);
long rc = sc_kpm_load(super_key.get(), module_path.get(), args.get(), nullptr);
if (rc < 0) [[unlikely]] {
LOGE("nativeLoadKernelPatchModule error: %ld", rc);
}
return rc;
}
jobject nativeControlKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr, jstring control_args_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto module_name = JUTFString(env, module_name_jstr);
const auto control_args = JUTFString(env, control_args_jstr);
char buf[4096] = { '\0' };
long rc = sc_kpm_control(super_key.get(), module_name.get(), control_args.get(), buf, sizeof(buf));
if (rc < 0) [[unlikely]] {
LOGE("nativeControlKernelPatchModule error: %ld", rc);
}
jclass cls = env->FindClass("me/bmax/apatch/Natives$KPMCtlRes");
jmethodID constructor = env->GetMethodID(cls, "<init>", "()V");
jfieldID rcField = env->GetFieldID(cls, "rc", "J");
jfieldID outMsg = env->GetFieldID(cls, "outMsg", "Ljava/lang/String;");
jobject obj = env->NewObject(cls, constructor);
env->SetLongField(obj, rcField, rc);
env->SetObjectField(obj, outMsg, env->NewStringUTF(buf));
return obj;
}
jlong nativeUnloadKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto module_name = JUTFString(env, module_name_jstr);
long rc = sc_kpm_unload(super_key.get(), module_name.get(), nullptr);
if (rc < 0) [[unlikely]] {
LOGE("nativeUnloadKernelPatchModule error: %ld", rc);
}
return rc;
}
jlong nativeKernelPatchModuleNum(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
long rc = sc_kpm_nums(super_key.get());
if (rc < 0) [[unlikely]] {
LOGE("nativeKernelPatchModuleNum error: %ld", rc);
}
return rc;
}
jstring nativeKernelPatchModuleList(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
char buf[4096] = { '\0' };
long rc = sc_kpm_list(super_key.get(), buf, sizeof(buf));
if (rc < 0) [[unlikely]] {
LOGE("nativeKernelPatchModuleList error: %ld", rc);
}
return env->NewStringUTF(buf);
}
jstring nativeKernelPatchModuleInfo(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto module_name = JUTFString(env, module_name_jstr);
char buf[1024] = { '\0' };
long rc = sc_kpm_info(super_key.get(), module_name.get(), buf, sizeof(buf));
if (rc < 0) [[unlikely]] {
LOGE("nativeKernelPatchModuleInfo error: %ld", rc);
}
return env->NewStringUTF(buf);
}
jlong nativeGrantSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid, jint to_uid, jstring selinux_context_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto selinux_context = JUTFString(env, selinux_context_jstr);
struct su_profile profile{};
profile.uid = uid;
profile.to_uid = to_uid;
if (selinux_context) strncpy(profile.scontext, selinux_context, sizeof(profile.scontext) - 1);
return sc_su_grant_uid(super_key.get(), &profile);
}
jlong nativeRevokeSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
return sc_su_revoke_uid(super_key.get(), (uid_t) uid);
}
jstring nativeSuPath(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
char buf[SU_PATH_MAX_LEN] = { '\0' };
long rc = sc_su_get_path(super_key.get(), buf, sizeof(buf));
if (rc < 0) [[unlikely]] {
LOGE("nativeSuPath error: %ld", rc);
}
return env->NewStringUTF(buf);
}
jboolean nativeResetSuPath(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring su_path_jstr) {
ensureSuperKeyNonNull(super_key_jstr);
const auto super_key = JUTFString(env, super_key_jstr);
const auto su_path = JUTFString(env, su_path_jstr);
return sc_su_reset_path(super_key.get(), su_path.get()) == 0;
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void * /*reserved*/) {
LOGI("Enter OnLoad");
JNIEnv* env{};
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) [[unlikely]] {
LOGE("Get JNIEnv error!");
return JNI_FALSE;
}
auto clazz = JNI_FindClass(env, "me/bmax/apatch/Natives");
if (clazz.get() == nullptr) [[unlikely]] {
LOGE("Failed to find Natives class");
return JNI_FALSE;
}
const static JNINativeMethod gMethods[] = {
{"nativeReady", "(Ljava/lang/String;)Z", reinterpret_cast<void *>(&nativeReady)},
{"nativeKernelPatchVersion", "(Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeKernelPatchVersion)},
{"nativeKernelPatchBuildTime", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchBuildTime)},
{"nativeSu", "(Ljava/lang/String;ILjava/lang/String;)J", reinterpret_cast<void *>(&nativeSu)},
{"nativeSetUidExclude", "(Ljava/lang/String;II)I", reinterpret_cast<void *>(&nativeSetUidExclude)},
{"nativeGetUidExclude", "(Ljava/lang/String;I)I", reinterpret_cast<void *>(&nativeGetUidExclude)},
{"nativeSuUids", "(Ljava/lang/String;)[I", reinterpret_cast<void *>(&nativeSuUids)},
{"nativeSuProfile", "(Ljava/lang/String;I)Lme/bmax/apatch/Natives$Profile;", reinterpret_cast<void *>(&nativeSuProfile)},
{"nativeLoadKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeLoadKernelPatchModule)},
{"nativeControlKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/bmax/apatch/Natives$KPMCtlRes;", reinterpret_cast<void *>(&nativeControlKernelPatchModule)},
{"nativeUnloadKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeUnloadKernelPatchModule)},
{"nativeKernelPatchModuleNum", "(Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeKernelPatchModuleNum)},
{"nativeKernelPatchModuleList", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchModuleList)},
{"nativeKernelPatchModuleInfo", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchModuleInfo)},
{"nativeGrantSu", "(Ljava/lang/String;IILjava/lang/String;)J", reinterpret_cast<void *>(&nativeGrantSu)},
{"nativeRevokeSu", "(Ljava/lang/String;I)J", reinterpret_cast<void *>(&nativeRevokeSu)},
{"nativeSuPath", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeSuPath)},
{"nativeResetSuPath", "(Ljava/lang/String;Ljava/lang/String;)Z", reinterpret_cast<void *>(&nativeResetSuPath)},
};
if (JNI_RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) [[unlikely]] {
LOGE("Failed to register native methods");
return JNI_FALSE;
}
LOGI("JNI_OnLoad Done!");
return JNI_VERSION_1_6;
}

View file

@ -0,0 +1,28 @@
//
// Created by GarfieldHan on 2024/6/11.
//
#ifndef APATCH_APJNI_HPP
#define APATCH_APJNI_HPP
#include <jni.h>
#include <android/log.h>
#include "jni_helper.hpp"
using namespace lsplant;
#define LOG_TAG "APatchNative"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
void ensureSuperKeyNonNull(jstring super_key_jstr) {
if (!super_key_jstr) [[unlikely]] {
LOGE("[%s] Super Key is null!", __PRETTY_FUNCTION__);
abort();
}
}
#endif //APATCH_APJNI_HPP

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,568 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2023 bmax121. All Rights Reserved.
*/
#ifndef _KPU_SUPERCALL_H_
#define _KPU_SUPERCALL_H_
#include <unistd.h>
#include <sys/syscall.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include "uapi/scdefs.h"
#include "version"
/// KernelPatch version is greater than or equal to 0x0a05
static inline long ver_and_cmd(const char *key, long cmd)
{
uint32_t version_code = (MAJOR << 16) + (MINOR << 8) + PATCH;
return ((long)version_code << 32) | (0x1158 << 16) | (cmd & 0xFFFF);
}
/**
* @brief If KernelPatch installed, @see SUPERCALL_HELLO_ECHO will echoed.
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @return long
*/
static inline long sc_hello(const char *key)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_HELLO));
return ret;
}
/**
* @brief Is KernelPatch installed?
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @return true
* @return false
*/
static inline bool sc_ready(const char *key)
{
return sc_hello(key) == SUPERCALL_HELLO_MAGIC;
}
/**
* @brief Print messages by printk in the kernel
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param msg
* @return long
*/
static inline long sc_klog(const char *key, const char *msg)
{
if (!key || !key[0]) return -EINVAL;
if (!msg || strlen(msg) <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KLOG), msg);
return ret;
}
/**
* @brief Print build kernel time
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param buildtime
* @param timestamp
* @return long
*/
static inline long sc_get_build_time(const char *key, const char *buildtime, size_t len)
{
if (!key || !key[0]) return -EINVAL;
if (!buildtime) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_BUILD_TIME), buildtime,len);
return ret;
}
/**
* @brief KernelPatch version number
*
* @param key
* @return uint32_t
*/
static inline uint32_t sc_kp_ver(const char *key)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KERNELPATCH_VER));
return (uint32_t)ret;
}
/**
* @brief Kernel version number
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @return uint32_t
*/
static inline uint32_t sc_k_ver(const char *key)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KERNEL_VER));
return (uint32_t)ret;
}
/**
* @brief Substitute user of current thread
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
* @see struct su_profile
* @return long : 0 if succeed
*/
static inline long sc_su(const char *key, struct su_profile *profile)
{
if (!key || !key[0]) return -EINVAL;
if (strlen(profile->scontext) >= SUPERCALL_SCONTEXT_LEN) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU), profile);
return ret;
}
/**
* @brief Substitute user of tid specfied thread
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param tid : target thread id
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
* @see struct su_profile
* @return long : 0 if succeed
*/
static inline long sc_su_task(const char *key, pid_t tid, struct su_profile *profile)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_TASK), tid, profile);
return ret;
}
/**
* @brief
*
* @param key
* @param gid group id
* @param did data id
* @param data
* @param dlen
* @return long
*/
static inline long sc_kstorage_write(const char *key, int gid, long did, void *data, int offset, int dlen)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_WRITE), gid, did, data, (((long)offset << 32) | dlen));
return ret;
}
/**
* @brief
*
* @param key
* @param gid
* @param did
* @param out_data
* @param dlen
* @return long
*/
static inline long sc_kstorage_read(const char *key, int gid, long did, void *out_data, int offset, int dlen)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_READ), gid, did, out_data, (((long)offset << 32) | dlen));
return ret;
}
/**
* @brief
*
* @param key
* @param gid
* @param ids
* @param ids_len
* @return long numbers of listed ids
*/
static inline long sc_kstorage_list_ids(const char *key, int gid, long *ids, int ids_len)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_LIST_IDS), gid, ids, ids_len);
return ret;
}
/**
* @brief
*
* @param key
* @param gid
* @param did
* @return long
*/
static inline long sc_kstorage_remove(const char *key, int gid, long did)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_REMOVE), gid, did);
return ret;
}
#ifdef ANDROID
/**
* @brief
*
* @param key
* @param uid
* @param exclude
* @return long
*/
static inline long sc_set_ap_mod_exclude(const char *key, uid_t uid, int exclude)
{
if(exclude) {
return sc_kstorage_write(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid, &exclude, 0, sizeof(exclude));
} else {
return sc_kstorage_remove(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid);
}
}
/**
* @brief
*
* @param key
* @param uid
* @param exclude
* @return long
*/
static inline int sc_get_ap_mod_exclude(const char *key, uid_t uid)
{
int exclude = 0;
int rc = sc_kstorage_read(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid, &exclude, 0, sizeof(exclude));
if (rc < 0) return 0;
return exclude;
}
/**
*
*/
static inline int sc_list_ap_mod_exclude(const char *key, uid_t *uids, int uids_len)
{
if(uids_len < 0 || uids_len > 512) return -E2BIG;
long ids[uids_len];
int rc = sc_kstorage_list_ids(key, KSTORAGE_EXCLUDE_LIST_GROUP, ids, uids_len);
if (rc < 0) return 0;
for(int i = 0; i < rc; i ++) {
uids[i] = (uid_t)ids[i];
}
return rc;
}
#endif
/**
* @brief Grant su permission
*
* @param key
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
* @return long : 0 if succeed
*/
static inline long sc_su_grant_uid(const char *key, struct su_profile *profile)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GRANT_UID), profile);
return ret;
}
/**
* @brief Revoke su permission
*
* @param key
* @param uid
* @return long 0 if succeed
*/
static inline long sc_su_revoke_uid(const char *key, uid_t uid)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_REVOKE_UID), uid);
return ret;
}
/**
* @brief Get numbers of su allowed uids
*
* @param key
* @return long
*/
static inline long sc_su_uid_nums(const char *key)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_NUMS));
return ret;
}
/**
* @brief
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param buf
* @param num
* @return long : The numbers of uids if succeed, nagative value if failed
*/
static inline long sc_su_allow_uids(const char *key, uid_t *buf, int num)
{
if (!key || !key[0]) return -EINVAL;
if (!buf || num <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_LIST), buf, num);
return ret;
}
/**
* @brief Get su profile of specified uid
*
* @param key
* @param uid
* @param out_profile
* @return long : 0 if succeed
*/
static inline long sc_su_uid_profile(const char *key, uid_t uid, struct su_profile *out_profile)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_PROFILE), uid, out_profile);
return ret;
}
/**
* @brief Get full path of current 'su' command
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param out_path
* @param path_len
* @return long : The length of result string if succeed, negative if failed
*/
static inline long sc_su_get_path(const char *key, char *out_path, int path_len)
{
if (!key || !key[0]) return -EINVAL;
if (!out_path || path_len <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_PATH), out_path, path_len);
return ret;
}
/**
* @brief Reset full path of 'su' command
*
* @param key
* @param path
* @return long : 0 if succeed
*/
static inline long sc_su_reset_path(const char *key, const char *path)
{
if (!key || !key[0]) return -EINVAL;
if (!path || !path[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_RESET_PATH), path);
return ret;
}
/**
* @brief Get current all-allowed selinux context
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param out_sctx
* @param sctx_len
* @return long 0 if there is a all-allowed selinux context now
*/
static inline long sc_su_get_all_allow_sctx(const char *key, char *out_sctx, int sctx_len)
{
if (!key || !key[0]) return -EINVAL;
if (!out_sctx) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_ALLOW_SCTX), out_sctx);
return ret;
}
/**
* @brief Reset current all-allowed selinux context
*
* @param key : superkey or 'su' string if caller uid is su allowed
* @param sctx If sctx is empty string, clear all-allowed selinux,
* otherwise, try to reset a new all-allowed selinux context
* @return long 0 if succeed
*/
static inline long sc_su_reset_all_allow_sctx(const char *key, const char *sctx)
{
if (!key || !key[0]) return -EINVAL;
if (!sctx) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_SET_ALLOW_SCTX), sctx);
return ret;
}
/**
* @brief Load module
*
* @param key : superkey
* @param path
* @param args
* @param reserved
* @return long : 0 if succeed
*/
static inline long sc_kpm_load(const char *key, const char *path, const char *args, void *reserved)
{
if (!key || !key[0]) return -EINVAL;
if (!path || strlen(path) <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_LOAD), path, args, reserved);
return ret;
}
/**
* @brief Control module with arguments
*
* @param key : superkey
* @param name : module name
* @param ctl_args : control argument
* @param out_msg : output message buffer
* @param outlen : buffer length of out_msg
* @return long : 0 if succeed
*/
static inline long sc_kpm_control(const char *key, const char *name, const char *ctl_args, char *out_msg, long outlen)
{
if (!key || !key[0]) return -EINVAL;
if (!name || strlen(name) <= 0) return -EINVAL;
if (!ctl_args || strlen(ctl_args) <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_CONTROL), name, ctl_args, out_msg, outlen);
return ret;
}
/**
* @brief Unload module
*
* @param key : superkey
* @param name : module name
* @param reserved
* @return long : 0 if succeed
*/
static inline long sc_kpm_unload(const char *key, const char *name, void *reserved)
{
if (!key || !key[0]) return -EINVAL;
if (!name || strlen(name) <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_UNLOAD), name, reserved);
return ret;
}
/**
* @brief Current loaded module numbers
*
* @param key : superkey
* @return long
*/
static inline long sc_kpm_nums(const char *key)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_NUMS));
return ret;
}
/**
* @brief List names of current loaded modules, splited with '\n'
*
* @param key : superkey
* @param names_buf : output buffer
* @param buf_len : the length of names_buf
* @return long : the length of result string if succeed, negative if failed
*/
static inline long sc_kpm_list(const char *key, char *names_buf, int buf_len)
{
if (!key || !key[0]) return -EINVAL;
if (!names_buf || buf_len <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_LIST), names_buf, buf_len);
return ret;
}
/**
* @brief Get module information.
*
* @param key : superkey
* @param name : module name
* @param buf :
* @param buf_len :
* @return long : The length of result string if succeed, negative if failed
*/
static inline long sc_kpm_info(const char *key, const char *name, char *buf, int buf_len)
{
if (!key || !key[0]) return -EINVAL;
if (!buf || buf_len <= 0) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_INFO), name, buf, buf_len);
return ret;
}
/**
* @brief Get current superkey
*
* @param key : superkey
* @param out_key
* @param outlen
* @return long : 0 if succeed
*/
static inline long sc_skey_get(const char *key, char *out_key, int outlen)
{
if (!key || !key[0]) return -EINVAL;
if (outlen < SUPERCALL_KEY_MAX_LEN) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_GET), out_key, outlen);
return ret;
}
/**
* @brief Reset current superkey
*
* @param key : superkey
* @param new_key
* @return long : 0 if succeed
*/
static inline long sc_skey_set(const char *key, const char *new_key)
{
if (!key || !key[0]) return -EINVAL;
if (!new_key || !new_key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_SET), new_key);
return ret;
}
/**
* @brief Whether to enable hash verification for root superkey.
*
* @param key : superkey
* @param enable
* @return long
*/
static inline long sc_skey_root_enable(const char *key, bool enable)
{
if (!key || !key[0]) return -EINVAL;
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_ROOT_ENABLE), (long)enable);
return ret;
}
/**
* @brief Get whether in safe mode
*
* @param key
* @return long
*/
static inline long sc_su_get_safemode(const char *key)
{
if (!key || !key[0]) return -EINVAL;
return syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_SAFEMODE));
}
static inline long sc_bootlog(const char *key)
{
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_BOOTLOG));
return ret;
}
static inline long sc_panic(const char *key)
{
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_PANIC));
return ret;
}
static inline long __sc_test(const char *key, long a1, long a2, long a3)
{
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_TEST), a1, a2, a3);
return ret;
}
#endif

View file

@ -0,0 +1,14 @@
#pragma once
#include <type_traits>
namespace lsplant {
template <class, template <class, class...> class>
struct is_instance : public std::false_type {};
template <class... Ts, template <class, class...> class U>
struct is_instance<U<Ts...>, U> : public std::true_type {};
template <class T, template <class, class...> class U>
inline constexpr bool is_instance_v = is_instance<T, U>::value;
} // namespace lsplant

View file

@ -0,0 +1,118 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* Copyright (C) 2023 bmax121. All Rights Reserved.
*/
#ifndef _KP_UAPI_SCDEF_H_
#define _KP_UAPI_SCDEF_H_
static inline long hash_key(const char *key)
{
long hash = 1000000007;
for (int i = 0; key[i]; i++) {
hash = hash * 31 + key[i];
}
return hash;
}
#define SUPERCALL_HELLO_ECHO "hello1158"
// #define __NR_supercall __NR3264_truncate // 45
#define __NR_supercall 45
#define SUPERCALL_HELLO 0x1000
#define SUPERCALL_KLOG 0x1004
#define SUPERCALL_BUILD_TIME 0x1007
#define SUPERCALL_KERNELPATCH_VER 0x1008
#define SUPERCALL_KERNEL_VER 0x1009
#define SUPERCALL_SKEY_GET 0x100a
#define SUPERCALL_SKEY_SET 0x100b
#define SUPERCALL_SKEY_ROOT_ENABLE 0x100c
#define SUPERCALL_SU 0x1010
#define SUPERCALL_SU_TASK 0x1011 // syscall(__NR_gettid)
#define SUPERCALL_KPM_LOAD 0x1020
#define SUPERCALL_KPM_UNLOAD 0x1021
#define SUPERCALL_KPM_CONTROL 0x1022
#define SUPERCALL_KPM_NUMS 0x1030
#define SUPERCALL_KPM_LIST 0x1031
#define SUPERCALL_KPM_INFO 0x1032
struct kernel_storage
{
void *data;
int len;
};
#define SUPERCALL_KSTORAGE_ALLOC_GROUP 0x1040
#define SUPERCALL_KSTORAGE_WRITE 0x1041
#define SUPERCALL_KSTORAGE_READ 0x1042
#define SUPERCALL_KSTORAGE_LIST_IDS 0x1043
#define SUPERCALL_KSTORAGE_REMOVE 0x1044
#define SUPERCALL_KSTORAGE_REMOVE_GROUP 0x1045
#define KSTORAGE_SU_LIST_GROUP 0
#define KSTORAGE_EXCLUDE_LIST_GROUP 1
#define KSTORAGE_UNUSED_GROUP_2 2
#define KSTORAGE_UNUSED_GROUP_3 3
#define SUPERCALL_BOOTLOG 0x10fd
#define SUPERCALL_PANIC 0x10fe
#define SUPERCALL_TEST 0x10ff
#define SUPERCALL_KEY_MAX_LEN 0x40
#define SUPERCALL_SCONTEXT_LEN 0x60
struct su_profile
{
uid_t uid;
uid_t to_uid;
char scontext[SUPERCALL_SCONTEXT_LEN];
};
#ifdef ANDROID
#define SH_PATH "/system/bin/sh"
#define SU_PATH "/system/bin/kp"
#define LEGACY_SU_PATH "/system/bin/su"
#define ECHO_PATH "/system/bin/echo"
#define KERNELPATCH_DATA_DIR "/data/adb/kp"
#define KERNELPATCH_MODULE_DATA_DIR KERNELPATCH_DATA_DIR "/modules"
#define APD_PATH "/data/adb/apd"
#define ALL_ALLOW_SCONTEXT "u:r:kp:s0"
#define ALL_ALLOW_SCONTEXT_MAGISK "u:r:magisk:s0"
#define ALL_ALLOW_SCONTEXT_KERNEL "u:r:kernel:s0"
#else
#define SH_PATH "/usr/bin/sh"
#define ECHO_PATH "/usr/bin/echo"
#define SU_PATH "/usr/bin/kp"
#define ALL_ALLOW_SCONTEXT "u:r:kernel:s0"
#endif
#define SU_PATH_MAX_LEN 128
#define SUPERCMD "/system/bin/truncate"
#define SAFE_MODE_FLAG_FILE "/dev/.safemode"
#define SUPERCALL_SU_GRANT_UID 0x1100
#define SUPERCALL_SU_REVOKE_UID 0x1101
#define SUPERCALL_SU_NUMS 0x1102
#define SUPERCALL_SU_LIST 0x1103
#define SUPERCALL_SU_PROFILE 0x1104
#define SUPERCALL_SU_GET_ALLOW_SCTX 0x1105
#define SUPERCALL_SU_SET_ALLOW_SCTX 0x1106
#define SUPERCALL_SU_GET_PATH 0x1110
#define SUPERCALL_SU_RESET_PATH 0x1111
#define SUPERCALL_SU_GET_SAFEMODE 0x1112
#define SUPERCALL_MAX 0x1200
#define SUPERCALL_RES_SUCCEED 0
#define SUPERCALL_HELLO_MAGIC 0x11581158
#endif

3
app/src/main/cpp/version Normal file
View file

@ -0,0 +1,3 @@
#define MAJOR 0
#define MINOR 12
#define PATCH 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,308 @@
package me.bmax.apatch
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.superuser.CallbackList
import me.bmax.apatch.ui.CrashHandleActivity
import me.bmax.apatch.util.APatchCli
import me.bmax.apatch.util.APatchKeyHelper
import me.bmax.apatch.util.Version
import me.bmax.apatch.util.getRootShell
import me.bmax.apatch.util.rootShellForResult
import me.bmax.apatch.util.verifyAppSignature
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.system.exitProcess
lateinit var apApp: APApplication
const val TAG = "APatch"
class APApplication : Application(), Thread.UncaughtExceptionHandler {
lateinit var okhttpClient: OkHttpClient
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
enum class State {
UNKNOWN_STATE,
KERNELPATCH_INSTALLED, KERNELPATCH_NEED_UPDATE, KERNELPATCH_NEED_REBOOT, KERNELPATCH_UNINSTALLING,
ANDROIDPATCH_NOT_INSTALLED, ANDROIDPATCH_INSTALLED, ANDROIDPATCH_INSTALLING, ANDROIDPATCH_NEED_UPDATE, ANDROIDPATCH_UNINSTALLING,
}
companion object {
const val APD_PATH = "/data/adb/apd"
@Deprecated("No more KPatch ELF from 0.11.0-dev")
const val KPATCH_PATH = "/data/adb/kpatch"
const val SUPERCMD = "/system/bin/truncate"
const val APATCH_FOLDER = "/data/adb/ap/"
private const val APATCH_BIN_FOLDER = APATCH_FOLDER + "bin/"
private const val APATCH_LOG_FOLDER = APATCH_FOLDER + "log/"
private const val APD_LINK_PATH = APATCH_BIN_FOLDER + "apd"
const val PACKAGE_CONFIG_FILE = APATCH_FOLDER + "package_config"
const val SU_PATH_FILE = APATCH_FOLDER + "su_path"
const val SAFEMODE_FILE = "/dev/.safemode"
private const val NEED_REBOOT_FILE = "/dev/.need_reboot"
const val GLOBAL_NAMESPACE_FILE = "/data/adb/.global_namespace_enable"
const val LITE_MODE_FILE = "/data/adb/.litemode_enable"
const val FORCE_OVERLAYFS_FILE = "/data/adb/.overlayfs_enable"
const val KPMS_DIR = APATCH_FOLDER + "kpms/"
@Deprecated("Use 'apd -V'")
const val APATCH_VERSION_PATH = APATCH_FOLDER + "version"
private const val MAGISKPOLICY_BIN_PATH = APATCH_BIN_FOLDER + "magiskpolicy"
private const val BUSYBOX_BIN_PATH = APATCH_BIN_FOLDER + "busybox"
private const val RESETPROP_BIN_PATH = APATCH_BIN_FOLDER + "resetprop"
private const val MAGISKBOOT_BIN_PATH = APATCH_BIN_FOLDER + "magiskboot"
const val DEFAULT_SCONTEXT = "u:r:untrusted_app:s0"
const val MAGISK_SCONTEXT = "u:r:magisk:s0"
private const val DEFAULT_SU_PATH = "/system/bin/kp"
private const val LEGACY_SU_PATH = "/system/bin/su"
const val SP_NAME = "config"
private const val SHOW_BACKUP_WARN = "show_backup_warning"
lateinit var sharedPreferences: SharedPreferences
private val logCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
Log.d(TAG, s.toString())
}
}
private val _kpStateLiveData = MutableLiveData(State.UNKNOWN_STATE)
val kpStateLiveData: LiveData<State> = _kpStateLiveData
private val _apStateLiveData = MutableLiveData(State.UNKNOWN_STATE)
val apStateLiveData: LiveData<State> = _apStateLiveData
@Suppress("DEPRECATION")
fun uninstallApatch() {
if (_apStateLiveData.value != State.ANDROIDPATCH_INSTALLED) return
_apStateLiveData.value = State.ANDROIDPATCH_UNINSTALLING
Natives.resetSuPath(DEFAULT_SU_PATH)
val cmds = arrayOf(
"rm -f $APD_PATH",
"rm -f $KPATCH_PATH",
"rm -rf $APATCH_BIN_FOLDER",
"rm -rf $APATCH_LOG_FOLDER",
"rm -rf $APATCH_VERSION_PATH",
)
val shell = getRootShell()
shell.newJob().add(*cmds).to(logCallback, logCallback).exec()
Log.d(TAG, "APatch uninstalled...")
if (_kpStateLiveData.value == State.UNKNOWN_STATE) {
_apStateLiveData.postValue(State.UNKNOWN_STATE)
} else {
_apStateLiveData.postValue(State.ANDROIDPATCH_NOT_INSTALLED)
}
}
@Suppress("DEPRECATION")
fun installApatch() {
val state = _apStateLiveData.value
if (state != State.ANDROIDPATCH_NOT_INSTALLED && state != State.ANDROIDPATCH_NEED_UPDATE) {
return
}
_apStateLiveData.value = State.ANDROIDPATCH_INSTALLING
val nativeDir = apApp.applicationInfo.nativeLibraryDir
Natives.resetSuPath(LEGACY_SU_PATH)
val cmds = arrayOf(
"mkdir -p $APATCH_BIN_FOLDER",
"mkdir -p $APATCH_LOG_FOLDER",
"cp -f ${nativeDir}/libapd.so $APD_PATH",
"chmod +x $APD_PATH",
"ln -s $APD_PATH $APD_LINK_PATH",
"restorecon $APD_PATH",
"cp -f ${nativeDir}/libmagiskpolicy.so $MAGISKPOLICY_BIN_PATH",
"chmod +x $MAGISKPOLICY_BIN_PATH",
"cp -f ${nativeDir}/libresetprop.so $RESETPROP_BIN_PATH",
"chmod +x $RESETPROP_BIN_PATH",
"cp -f ${nativeDir}/libbusybox.so $BUSYBOX_BIN_PATH",
"chmod +x $BUSYBOX_BIN_PATH",
"cp -f ${nativeDir}/libmagiskboot.so $MAGISKBOOT_BIN_PATH",
"chmod +x $MAGISKBOOT_BIN_PATH",
"touch $PACKAGE_CONFIG_FILE",
"touch $SU_PATH_FILE",
"[ -s $SU_PATH_FILE ] || echo $LEGACY_SU_PATH > $SU_PATH_FILE",
"echo ${Version.getManagerVersion().second} > $APATCH_VERSION_PATH",
"restorecon -R $APATCH_FOLDER",
"${nativeDir}/libmagiskpolicy.so --magisk --live",
)
val shell = getRootShell()
shell.newJob().add(*cmds).to(logCallback, logCallback).exec()
// clear shell cache
APatchCli.refresh()
Log.d(TAG, "APatch installed...")
_apStateLiveData.postValue(State.ANDROIDPATCH_INSTALLED)
}
fun markNeedReboot() {
val result = rootShellForResult("touch $NEED_REBOOT_FILE")
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_REBOOT)
Log.d(TAG, "mark reboot ${result.code}")
}
var superKey: String = ""
set(value) {
field = value
val ready = Natives.nativeReady(value)
_kpStateLiveData.value =
if (ready) State.KERNELPATCH_INSTALLED else State.UNKNOWN_STATE
_apStateLiveData.value =
if (ready) State.ANDROIDPATCH_NOT_INSTALLED else State.UNKNOWN_STATE
Log.d(TAG, "state: " + _kpStateLiveData.value)
if (!ready) return
APatchKeyHelper.writeSPSuperKey(value)
thread {
val rc = Natives.su(0, null)
if (!rc) {
Log.e(TAG, "Native.su failed")
return@thread
}
// KernelPatch version
//val buildV = Version.buildKPVUInt()
//val installedV = Version.installedKPVUInt()
//use build time to check update
val buildV = Version.getKpImg()
val installedV = Version.installedKPTime()
Log.d(TAG, "kp installed version: ${installedV}, build version: $buildV")
// use != instead of > to enable downgrade,
if (buildV != installedV) {
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_UPDATE)
}
Log.d(TAG, "kp state: " + _kpStateLiveData.value)
if (File(NEED_REBOOT_FILE).exists()) {
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_REBOOT)
}
Log.d(TAG, "kp state: " + _kpStateLiveData.value)
// AndroidPatch version
val mgv = Version.getManagerVersion().second
val installedApdVInt = Version.installedApdVUInt()
Log.d(TAG, "manager version: $mgv, installed apd version: $installedApdVInt")
if (Version.installedApdVInt > 0) {
_apStateLiveData.postValue(State.ANDROIDPATCH_INSTALLED)
}
if (Version.installedApdVInt > 0 && mgv.toInt() != Version.installedApdVInt) {
_apStateLiveData.postValue(State.ANDROIDPATCH_NEED_UPDATE)
// su path
val suPathFile = File(SU_PATH_FILE)
if (suPathFile.exists()) {
val suPath = suPathFile.readLines()[0].trim()
if (Natives.suPath() != suPath) {
Log.d(TAG, "su path: $suPath")
Natives.resetSuPath(suPath)
}
}
}
Log.d(TAG, "ap state: " + _apStateLiveData.value)
return@thread
}
}
}
override fun onCreate() {
super.onCreate()
apApp = this
val isArm64 = Build.SUPPORTED_ABIS.any { it == "arm64-v8a" }
if (!isArm64) {
Toast.makeText(applicationContext, "Unsupported architecture!", Toast.LENGTH_LONG)
.show()
Thread.sleep(5000)
exitProcess(0)
}
if (!BuildConfig.DEBUG && !verifyAppSignature("1x2twMoHvfWUODv7KkRRNKBzOfEqJwRKGzJpgaz18xk=")) {
while (true) {
val intent = Intent(Intent.ACTION_DELETE)
intent.data = "package:$packageName".toUri()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
startActivity(intent)
exitProcess(0)
}
}
// TODO: We can't totally protect superkey from be stolen by root or LSPosed-like injection tools in user space, the only way is don't use superkey,
// TODO: 1. make me root by kernel
// TODO: 2. remove all usage of superkey
sharedPreferences = getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
APatchKeyHelper.setSharedPreferences(sharedPreferences)
superKey = APatchKeyHelper.readSPSuperKey()
okhttpClient =
OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024))
.addInterceptor { block ->
block.proceed(
block.request().newBuilder()
.header("User-Agent", "APatch/${BuildConfig.VERSION_CODE}")
.header("Accept-Language", Locale.getDefault().toLanguageTag()).build()
)
}.build()
}
fun getBackupWarningState(): Boolean {
return sharedPreferences.getBoolean(SHOW_BACKUP_WARN, true)
}
fun updateBackupWarningState(state: Boolean) {
sharedPreferences.edit { putBoolean(SHOW_BACKUP_WARN, state) }
}
override fun uncaughtException(t: Thread, e: Throwable) {
val exceptionMessage = Log.getStackTraceString(e)
val threadName = t.name
Log.e(TAG, "Error on thread $threadName:\n $exceptionMessage")
val intent = Intent(this, CrashHandleActivity::class.java).apply {
putExtra("exception_message", exceptionMessage)
putExtra("thread", threadName)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
exitProcess(10)
}
}

View file

@ -0,0 +1,155 @@
package me.bmax.apatch
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.compose.runtime.Immutable
import dalvik.annotation.optimization.FastNative
import kotlinx.parcelize.Parcelize
object Natives {
init {
System.loadLibrary("apjni")
}
@Immutable
@Parcelize
@Keep
data class Profile(
var uid: Int = 0,
var toUid: Int = 0,
var scontext: String = APApplication.DEFAULT_SCONTEXT,
) : Parcelable
@Keep
class KPMCtlRes {
var rc: Long = 0
var outMsg: String? = null
constructor()
constructor(rc: Long, outMsg: String?) {
this.rc = rc
this.outMsg = outMsg
}
}
@FastNative
private external fun nativeSu(superKey: String, toUid: Int, scontext: String?): Long
fun su(toUid: Int, scontext: String?): Boolean {
return nativeSu(APApplication.superKey, toUid, scontext) == 0L
}
fun su(): Boolean {
return su(0, "")
}
@FastNative
external fun nativeReady(superKey: String): Boolean
@FastNative
private external fun nativeSuPath(superKey: String): String
fun suPath(): String {
return nativeSuPath(APApplication.superKey)
}
@FastNative
private external fun nativeSuUids(superKey: String): IntArray
fun suUids(): IntArray {
return nativeSuUids(APApplication.superKey)
}
@FastNative
private external fun nativeKernelPatchVersion(superKey: String): Long
fun kernelPatchVersion(): Long {
return nativeKernelPatchVersion(APApplication.superKey)
}
@FastNative
private external fun nativeKernelPatchBuildTime(superKey: String): String
fun kernelPatchBuildTime(): String {
return nativeKernelPatchBuildTime(APApplication.superKey)
}
private external fun nativeLoadKernelPatchModule(
superKey: String, modulePath: String, args: String
): Long
fun loadKernelPatchModule(modulePath: String, args: String): Long {
return nativeLoadKernelPatchModule(APApplication.superKey, modulePath, args)
}
private external fun nativeUnloadKernelPatchModule(superKey: String, moduleName: String): Long
fun unloadKernelPatchModule(moduleName: String): Long {
return nativeUnloadKernelPatchModule(APApplication.superKey, moduleName)
}
@FastNative
private external fun nativeKernelPatchModuleNum(superKey: String): Long
fun kernelPatchModuleNum(): Long {
return nativeKernelPatchModuleNum(APApplication.superKey)
}
@FastNative
private external fun nativeKernelPatchModuleList(superKey: String): String
fun kernelPatchModuleList(): String {
return nativeKernelPatchModuleList(APApplication.superKey)
}
@FastNative
private external fun nativeKernelPatchModuleInfo(superKey: String, moduleName: String): String
fun kernelPatchModuleInfo(moduleName: String): String {
return nativeKernelPatchModuleInfo(APApplication.superKey, moduleName)
}
private external fun nativeControlKernelPatchModule(
superKey: String, modName: String, jctlargs: String
): KPMCtlRes
fun kernelPatchModuleControl(moduleName: String, controlArg: String): KPMCtlRes {
return nativeControlKernelPatchModule(APApplication.superKey, moduleName, controlArg)
}
@FastNative
private external fun nativeGrantSu(
superKey: String, uid: Int, toUid: Int, scontext: String?
): Long
fun grantSu(uid: Int, toUid: Int, scontext: String?): Long {
return nativeGrantSu(APApplication.superKey, uid, toUid, scontext)
}
@FastNative
private external fun nativeRevokeSu(superKey: String, uid: Int): Long
fun revokeSu(uid: Int): Long {
return nativeRevokeSu(APApplication.superKey, uid)
}
@FastNative
private external fun nativeSetUidExclude(superKey: String, uid: Int, exclude: Int): Int
fun setUidExclude(uid: Int, exclude: Int): Int {
return nativeSetUidExclude(APApplication.superKey, uid, exclude)
}
@FastNative
private external fun nativeGetUidExclude(superKey: String, uid: Int): Int
fun isUidExcluded(uid: Int): Int {
return nativeGetUidExclude(APApplication.superKey, uid)
}
@FastNative
private external fun nativeSuProfile(superKey: String, uid: Int): Profile
fun suProfile(uid: Int): Profile {
return nativeSuProfile(APApplication.superKey, uid)
}
@FastNative
private external fun nativeResetSuPath(superKey: String, path: String): Boolean
fun resetSuPath(path: String): Boolean {
return nativeResetSuPath(APApplication.superKey, path)
}
}

View file

@ -0,0 +1,72 @@
package me.bmax.apatch.services;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.ipc.RootService;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import me.bmax.apatch.IAPRootService;
import rikka.parcelablelist.ParcelableListSlice;
public class RootServices extends RootService {
private static final String TAG = "RootServices";
@Override
public IBinder onBind(@NonNull Intent intent) {
return new Stub();
}
List<Integer> getUserIds() {
List<Integer> result = new ArrayList<>();
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
List<UserHandle> userProfiles = um.getUserProfiles();
for (UserHandle userProfile : userProfiles) {
int userId = userProfile.hashCode();
result.add(userProfile.hashCode());
}
return result;
}
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
ArrayList<PackageInfo> packages = new ArrayList<>();
for (Integer userId : getUserIds()) {
Log.i(TAG, "getInstalledPackagesAll: " + userId);
packages.addAll(getInstalledPackagesAsUser(flags, userId));
}
return packages;
}
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
try {
PackageManager pm = getPackageManager();
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
} catch (Throwable e) {
Log.e(TAG, "err", e);
}
return new ArrayList<>();
}
class Stub extends IAPRootService.Stub {
@Override
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
List<PackageInfo> list = getInstalledPackagesAll(flags);
Log.i(TAG, "getPackages: " + list.size());
return new ParcelableListSlice<>(list);
}
}
}

View file

@ -0,0 +1,151 @@
package me.bmax.apatch.ui
import android.content.ClipData
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.R
import me.bmax.apatch.ui.theme.APatchTheme
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class CrashHandleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val appName = getString(R.string.app_name)
val versionName = BuildConfig.VERSION_NAME
val versionCode = BuildConfig.VERSION_CODE
val deviceBrand = Build.BRAND
val deviceModel = Build.MODEL
val sdkLevel = Build.VERSION.SDK_INT
val currentDateTime = Calendar.getInstance().time
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val formattedDateTime = formatter.format(currentDateTime)
val exceptionMessage = intent.getStringExtra("exception_message")
val threadName = intent.getStringExtra("thread")
val message = buildString {
append(appName).append(" version: ").append(versionName).append(" ($versionCode)")
.append("\n\n")
append("Brand: ").append(deviceBrand).append("\n")
append("Model: ").append(deviceModel).append("\n")
append("SDK Level: ").append(sdkLevel).append("\n")
append("Time: ").append(formattedDateTime).append("\n\n")
append("Thread: ").append(threadName).append("\n")
append("Crash Info: \n").append(exceptionMessage)
}
setContent {
APatchTheme {
CrashHandleScreen(message)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CrashHandleScreen(
message: String
) {
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
LargeTopAppBar(
title = { Text(text = stringResource(R.string.crash_handle_title)) },
scrollBehavior = scrollBehavior,
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
)
}, floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
scope.launch {
clipboard.setClipEntry(
ClipEntry(ClipData.newPlainText("CrashLog", message)),
)
}
}, text = { Text(text = stringResource(R.string.crash_handle_copy)) }, icon = {
Icon(
imageVector = Icons.Outlined.ContentCopy, contentDescription = null
)
}, modifier = Modifier.windowInsetsPadding(
WindowInsets.safeDrawing.only(WindowInsetsSides.End)
)
)
}) {
SelectionContainer(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
.padding(it)
.padding(
start = 16.dp, top = 16.dp, end = 16.dp, bottom = 16.dp + 56.dp + 16.dp
)
) {
Text(
text = message, style = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = 11.sp
)
)
}
}
}
@Preview
@Composable
fun CrashHandleScreenPreview() {
APatchTheme {
CrashHandleScreen("Crash log here")
}
}

View file

@ -0,0 +1,209 @@
package me.bmax.apatch.ui
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.Crossfade
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import coil.Coil
import coil.ImageLoader
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.rememberNavHostEngine
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import me.bmax.apatch.APApplication
import me.bmax.apatch.ui.screen.BottomBarDestination
import me.bmax.apatch.ui.theme.APatchTheme
import me.bmax.apatch.util.ui.LocalSnackbarHost
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
class MainActivity : AppCompatActivity() {
private var isLoading = true
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition { isLoading }
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
setContent {
APatchTheme {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
val bottomBarRoutes = remember {
BottomBarDestination.entries.map { it.direction.route }.toSet()
}
Scaffold(
bottomBar = { BottomBar(navController) }
) { _ ->
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState,
) {
DestinationsNavHost(
modifier = Modifier.padding(bottom = 80.dp),
navGraph = NavGraphs.root,
navController = navController,
engine = rememberNavHostEngine(navHostContentAlignment = Alignment.TopCenter),
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
{
// If the target is a detail page (not a bottom navigation page), slide in from the right
if (targetState.destination.route !in bottomBarRoutes) {
slideInHorizontally(initialOffsetX = { it })
} else {
// Otherwise (switching between bottom navigation pages), use fade in
fadeIn(animationSpec = tween(340))
}
}
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
{
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
} else {
// Otherwise (switching between bottom navigation pages), use fade out
fadeOut(animationSpec = tween(340))
}
}
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
{
// If returning to the home page (bottom navigation page), slide in from the left
if (targetState.destination.route in bottomBarRoutes) {
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
} else {
// Otherwise (e.g., returning between multiple detail pages), use default fade in
fadeIn(animationSpec = tween(340))
}
}
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
{
// If returning from a detail page (not a bottom navigation page), scale down and fade out
if (initialState.destination.route !in bottomBarRoutes) {
scaleOut(targetScale = 0.9f) + fadeOut()
} else {
// Otherwise, use default fade out
fadeOut(animationSpec = tween(340))
}
}
}
)
}
}
}
}
// Initialize Coil
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(iconSize, false, this@MainActivity))
}
.build()
)
isLoading = false
}
}
@Composable
private fun BottomBar(navController: NavHostController) {
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
val navigator = navController.rememberDestinationsNavigator()
Crossfade(
targetState = state,
label = "BottomBarStateCrossfade"
) { state ->
val kPatchReady = state != APApplication.State.UNKNOWN_STATE
val aPatchReady = state == APApplication.State.ANDROIDPATCH_INSTALLED
NavigationBar(tonalElevation = 8.dp) {
BottomBarDestination.entries.forEach { destination ->
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
val hideDestination = (destination.kPatchRequired && !kPatchReady) || (destination.aPatchRequired && !aPatchReady)
if (hideDestination) return@forEach
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
},
label = {
Text(
text = stringResource(destination.label),
overflow = TextOverflow.Visible,
maxLines = 1,
softWrap = false
)
},
alwaysShowLabel = false
)
}
}
}
}

View file

@ -0,0 +1,89 @@
package me.bmax.apatch.ui
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup.MarginLayoutParams
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader
import me.bmax.apatch.APApplication
import me.bmax.apatch.ui.webui.SuFilePathHandler
import me.bmax.apatch.ui.webui.WebViewInterface
import java.io.File
@SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() {
private lateinit var webViewInterface: WebViewInterface
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val moduleId = intent.getStringExtra("id")!!
val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("APatch - $name"))
} else {
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("APatch - $name").build()
setTaskDescription(taskDescription)
}
val prefs = APApplication.sharedPreferences
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false))
val webRoot = File("/data/adb/modules/${moduleId}/webroot")
val webViewAssetLoader = WebViewAssetLoader.Builder()
.setDomain("mui.kernelsu.org")
.addPathHandler(
"/",
SuFilePathHandler(this, webRoot)
)
.build()
val webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url)
}
}
val webView = WebView(this).apply {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<MarginLayoutParams> {
leftMargin = inset.left
rightMargin = inset.right
topMargin = inset.top
bottomMargin = inset.bottom
}
return@setOnApplyWindowInsetsListener insets
}
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
webViewInterface = WebViewInterface(this@WebUIActivity, this)
addJavascriptInterface(webViewInterface, "ksu")
setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html")
}
setContentView(webView)
}
}

View file

@ -0,0 +1,533 @@
package me.bmax.apatch.ui.component
import android.graphics.text.LineBreaker
import android.os.Build
import android.os.Parcelable
import android.text.Layout
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.SecureFlagPolicy
import io.noties.markwon.Markwon
import io.noties.markwon.utils.NoCopySpannableFactory
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.parcelize.Parcelize
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils.Companion.setupWindowBlurListener
import kotlin.coroutines.resume
private const val TAG = "DialogComponent"
interface ConfirmDialogVisuals : Parcelable {
val title: String
val content: String
val isMarkdown: Boolean
val confirm: String?
val dismiss: String?
}
@Parcelize
private data class ConfirmDialogVisualsImpl(
override val title: String,
override val content: String,
override val isMarkdown: Boolean,
override val confirm: String?,
override val dismiss: String?,
) : ConfirmDialogVisuals {
companion object {
val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", false, null, null)
}
}
interface DialogHandle {
val isShown: Boolean
val dialogType: String
fun show()
fun hide()
}
interface LoadingDialogHandle : DialogHandle {
suspend fun <R> withLoading(block: suspend () -> R): R
fun showLoading()
}
sealed interface ConfirmResult {
data object Confirmed : ConfirmResult
data object Canceled : ConfirmResult
}
interface ConfirmDialogHandle : DialogHandle {
val visuals: ConfirmDialogVisuals
fun showConfirm(
title: String,
content: String,
markdown: Boolean = false,
confirm: String? = null,
dismiss: String? = null
)
suspend fun awaitConfirm(
title: String,
content: String,
markdown: Boolean = false,
confirm: String? = null,
dismiss: String? = null
): ConfirmResult
}
private abstract class DialogHandleBase(
protected val visible: MutableState<Boolean>,
protected val coroutineScope: CoroutineScope
) : DialogHandle {
override val isShown: Boolean
get() = visible.value
override fun show() {
coroutineScope.launch {
visible.value = true
}
}
final override fun hide() {
coroutineScope.launch {
visible.value = false
}
}
override fun toString(): String {
return dialogType
}
}
private class LoadingDialogHandleImpl(
visible: MutableState<Boolean>,
coroutineScope: CoroutineScope
) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) {
override suspend fun <R> withLoading(block: suspend () -> R): R {
return coroutineScope.async {
try {
visible.value = true
block()
} finally {
visible.value = false
}
}.await()
}
override fun showLoading() {
show()
}
override val dialogType: String get() = "LoadingDialog"
}
typealias NullableCallback = (() -> Unit)?
interface ConfirmCallback {
val onConfirm: NullableCallback
val onDismiss: NullableCallback
val isEmpty: Boolean get() = onConfirm == null && onDismiss == null
companion object {
operator fun invoke(
onConfirmProvider: () -> NullableCallback,
onDismissProvider: () -> NullableCallback
): ConfirmCallback {
return object : ConfirmCallback {
override val onConfirm: NullableCallback
get() = onConfirmProvider()
override val onDismiss: NullableCallback
get() = onDismissProvider()
}
}
}
}
private class ConfirmDialogHandleImpl(
visible: MutableState<Boolean>,
coroutineScope: CoroutineScope,
callback: ConfirmCallback,
override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty,
private val resultFlow: ReceiveChannel<ConfirmResult>
) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) {
private class ResultCollector(
private val callback: ConfirmCallback
) : FlowCollector<ConfirmResult> {
fun handleResult(result: ConfirmResult) {
Log.d(TAG, "handleResult: ${result.javaClass.simpleName}")
when (result) {
ConfirmResult.Confirmed -> onConfirm()
ConfirmResult.Canceled -> onDismiss()
}
}
fun onConfirm() {
callback.onConfirm?.invoke()
}
fun onDismiss() {
callback.onDismiss?.invoke()
}
override suspend fun emit(value: ConfirmResult) {
handleResult(value)
}
}
private val resultCollector = ResultCollector(callback)
private var awaitContinuation: CancellableContinuation<ConfirmResult>? = null
private val isCallbackEmpty = callback.isEmpty
init {
coroutineScope.launch {
resultFlow
.consumeAsFlow()
.onEach { result ->
awaitContinuation?.let {
awaitContinuation = null
if (it.isActive) {
it.resume(result)
}
}
}
.onEach { hide() }
.collect(resultCollector)
}
}
private suspend fun awaitResult(): ConfirmResult {
return suspendCancellableCoroutine {
awaitContinuation = it.apply {
if (isCallbackEmpty) {
invokeOnCancellation {
visible.value = false
}
}
}
}
}
fun updateVisuals(visuals: ConfirmDialogVisuals) {
this.visuals = visuals
}
override fun show() {
if (visuals !== ConfirmDialogVisualsImpl.Empty) {
super.show()
} else {
throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals")
}
}
override fun showConfirm(
title: String,
content: String,
markdown: Boolean,
confirm: String?,
dismiss: String?
) {
coroutineScope.launch {
updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss))
show()
}
}
override suspend fun awaitConfirm(
title: String,
content: String,
markdown: Boolean,
confirm: String?,
dismiss: String?
): ConfirmResult {
coroutineScope.launch {
updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss))
show()
}
return awaitResult()
}
override val dialogType: String get() = "ConfirmDialog"
override fun toString(): String {
return "${super.toString()}(visuals: $visuals)"
}
companion object {
fun Saver(
visible: MutableState<Boolean>,
coroutineScope: CoroutineScope,
callback: ConfirmCallback,
resultChannel: ReceiveChannel<ConfirmResult>
) = Saver<ConfirmDialogHandle, ConfirmDialogVisuals>(
save = {
it.visuals
},
restore = {
Log.d(TAG, "ConfirmDialog restore, visuals: $it")
ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel)
}
)
}
}
private class CustomDialogHandleImpl(
visible: MutableState<Boolean>,
coroutineScope: CoroutineScope
) : DialogHandleBase(visible, coroutineScope) {
override val dialogType: String get() = "CustomDialog"
}
@Composable
fun rememberLoadingDialog(): LoadingDialogHandle {
val visible = remember {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
if (visible.value) {
LoadingDialog()
}
return remember {
LoadingDialogHandleImpl(visible, coroutineScope)
}
}
@Composable
private fun rememberConfirmDialog(
visuals: ConfirmDialogVisuals,
callback: ConfirmCallback
): ConfirmDialogHandle {
val visible = rememberSaveable {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
val resultChannel = remember {
Channel<ConfirmResult>()
}
val handle = rememberSaveable(
saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel),
init = {
ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel)
}
)
if (visible.value) {
ConfirmDialog(
handle.visuals,
confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } },
dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }
)
}
return handle
}
@Composable
fun rememberConfirmCallback(
onConfirm: NullableCallback,
onDismiss: NullableCallback
): ConfirmCallback {
val currentOnConfirm by rememberUpdatedState(newValue = onConfirm)
val currentOnDismiss by rememberUpdatedState(newValue = onDismiss)
return remember {
ConfirmCallback({ currentOnConfirm }, { currentOnDismiss })
}
}
@Composable
fun rememberConfirmDialog(
onConfirm: NullableCallback = null,
onDismiss: NullableCallback = null
): ConfirmDialogHandle {
return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))
}
@Composable
fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {
return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback)
}
@Composable
fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle {
val visible = rememberSaveable {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
if (visible.value) {
composable { visible.value = false }
}
return remember {
CustomDialogHandleImpl(visible, coroutineScope)
}
}
@Composable
private fun LoadingDialog() {
Dialog(
onDismissRequest = {},
properties = DialogProperties(
dismissOnClickOutside = false,
dismissOnBackPress = false,
usePlatformDefaultWidth = false
)
) {
Surface(
modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
setupWindowBlurListener(dialogWindowProvider.window)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) {
BasicAlertDialog(
onDismissRequest = {
dismiss()
},
properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
securePolicy = SecureFlagPolicy.SecureOff
)
) {
Surface(
modifier = Modifier
.width(320.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(20.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.Start)
) {
Text(text = visuals.title, style = MaterialTheme.typography.headlineSmall)
}
Box(
Modifier
.weight(weight = 1f, fill = false)
.padding(PaddingValues(bottom = 24.dp))
.align(Alignment.Start)
) {
if (visuals.isMarkdown) {
MarkdownContent(content = visuals.content)
} else {
Text(text = visuals.content, style = MaterialTheme.typography.bodyMedium)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = dismiss) {
Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel))
}
TextButton(onClick = confirm) {
Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok))
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
setupWindowBlurListener(dialogWindowProvider.window)
}
}
}
@Composable
private fun MarkdownContent(content: String) {
val contentColor = LocalContentColor.current
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
)
}
},
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
update = {
Markwon.create(it.context).setMarkdown(it, content)
it.setTextColor(contentColor.toArgb())
}
)
}

View file

@ -0,0 +1,14 @@
package me.bmax.apatch.ui.component
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@Composable
fun ProvideMenuShape(
value: CornerBasedShape = RoundedCornerShape(8.dp), content: @Composable () -> Unit
) = MaterialTheme(
shapes = MaterialTheme.shapes.copy(extraSmall = value), content = content
)

View file

@ -0,0 +1,28 @@
package me.bmax.apatch.ui.component
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onKeyEvent
@Composable
fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) {
val requester = remember { FocusRequester() }
Box(
Modifier
.onKeyEvent {
predicate(it)
}
.focusRequester(requester)
.focusable()
)
LaunchedEffect(Unit) {
requester.requestFocus()
}
}

View file

@ -0,0 +1,98 @@
package me.bmax.apatch.ui.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import me.bmax.apatch.R
@Composable
fun ModuleUpdateButton(
onClick: () -> Unit
) = FilledTonalButton(
onClick = onClick, enabled = true, contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.device_mobile_down),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.apm_update),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
@Composable
fun ModuleRemoveButton(
enabled: Boolean, onClick: () -> Unit
) = FilledTonalButton(
onClick = onClick, enabled = enabled, contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.trash),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.apm_remove),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
@Composable
fun KPModuleRemoveButton(
enabled: Boolean, onClick: () -> Unit
) = FilledTonalButton(
onClick = onClick, enabled = enabled, contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.trash),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.kpm_unload),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
@Composable
fun ModuleStateIndicator(
@DrawableRes icon: Int, color: Color = MaterialTheme.colorScheme.outline
) {
Image(
modifier = Modifier.requiredSize(150.dp),
painter = painterResource(id = icon),
contentDescription = null,
alpha = 0.1f,
colorFilter = ColorFilter.tint(color)
)
}

View file

@ -0,0 +1,169 @@
package me.bmax.apatch.ui.component
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
private const val TAG = "SearchBar"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchAppBar(
title: @Composable () -> Unit,
searchText: String,
onSearchTextChange: (String) -> Unit,
onClearClick: () -> Unit,
onBackClick: (() -> Unit)? = null,
onConfirm: (() -> Unit)? = null,
dropdownContent: @Composable (() -> Unit)? = null,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
var onSearch by remember { mutableStateOf(false) }
if (onSearch) {
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
BackHandler(
enabled = onSearch,
onBack = {
keyboardController?.hide()
onClearClick()
onSearch = !onSearch
}
)
DisposableEffect(Unit) {
onDispose {
keyboardController?.hide()
}
}
TopAppBar(
title = {
Box {
AnimatedVisibility(
modifier = Modifier.align(Alignment.CenterStart),
visible = !onSearch,
enter = fadeIn(),
exit = fadeOut(),
content = { title() }
)
AnimatedVisibility(
visible = onSearch,
enter = fadeIn(),
exit = fadeOut()
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(
top = 2.dp,
bottom = 2.dp,
end = if (onBackClick != null) 0.dp else 14.dp
)
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
if (focusState.isFocused) onSearch = true
Log.d(TAG, "onFocusChanged: $focusState")
},
value = searchText,
onValueChange = onSearchTextChange,
shape = RoundedCornerShape(15.dp),
trailingIcon = {
IconButton(
onClick = {
onSearch = false
keyboardController?.hide()
onClearClick()
},
content = { Icon(Icons.Filled.Close, null) }
)
},
maxLines = 1,
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions {
defaultKeyboardAction(ImeAction.Search)
keyboardController?.hide()
onConfirm?.invoke()
},
)
}
}
},
navigationIcon = {
if (onBackClick != null) {
IconButton(
onClick = onBackClick,
content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) }
)
}
},
actions = {
AnimatedVisibility(
visible = !onSearch
) {
IconButton(
onClick = { onSearch = true },
content = { Icon(Icons.Filled.Search, null) }
)
}
dropdownContent?.invoke()
}
)
}
@Preview
@Composable
private fun SearchAppBarPreview() {
var searchText by remember { mutableStateOf("") }
SearchAppBar(
title = { Text("Search text") },
searchText = searchText,
onSearchTextChange = { searchText = it },
onClearClick = { searchText = "" }
)
}

View file

@ -0,0 +1,83 @@
package me.bmax.apatch.ui.component
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
@Composable
fun SwitchItem(
icon: ImageVector? = null,
title: String,
summary: String? = null,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
ListItem(
modifier = Modifier.toggleable(
value = checked,
interactionSource = interactionSource,
role = Role.Switch,
enabled = enabled,
indication = LocalIndication.current,
onValueChange = onCheckedChange
),
headlineContent = {
Text(
title,
style = MaterialTheme.typography.bodyLarge,
color = LocalContentColor.current
)
},
leadingContent = icon?.let {
{ Icon(icon, title) }
},
trailingContent = {
Switch(
checked = checked,
enabled = enabled,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource
)
},
supportingContent = {
if (summary != null) {
Text(
summary,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
}
)
}
@Composable
fun RadioItem(
title: String,
selected: Boolean,
onClick: () -> Unit,
) {
ListItem(
headlineContent = {
Text(title)
},
leadingContent = {
RadioButton(selected = selected, onClick = onClick)
},
)
}

View file

@ -0,0 +1,604 @@
package me.bmax.apatch.ui.screen
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.util.Patterns
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ExecuteAPMActionScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.APApplication
import me.bmax.apatch.R
import me.bmax.apatch.apApp
import me.bmax.apatch.ui.WebUIActivity
import me.bmax.apatch.ui.component.ConfirmResult
import me.bmax.apatch.ui.component.ModuleRemoveButton
import me.bmax.apatch.ui.component.ModuleStateIndicator
import me.bmax.apatch.ui.component.ModuleUpdateButton
import me.bmax.apatch.ui.component.rememberConfirmDialog
import me.bmax.apatch.ui.component.rememberLoadingDialog
import me.bmax.apatch.ui.viewmodel.APModuleViewModel
import me.bmax.apatch.util.DownloadListener
import me.bmax.apatch.util.download
import me.bmax.apatch.util.hasMagisk
import me.bmax.apatch.util.reboot
import me.bmax.apatch.util.toggleModule
import me.bmax.apatch.util.ui.LocalSnackbarHost
import me.bmax.apatch.util.uninstallModule
@Destination<RootGraph>
@Composable
fun APModuleScreen(navigator: DestinationsNavigator) {
val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
if (state != APApplication.State.ANDROIDPATCH_INSTALLED && state != APApplication.State.ANDROIDPATCH_NEED_UPDATE) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Text(
text = stringResource(id = R.string.apm_not_installed),
style = MaterialTheme.typography.titleMedium
)
}
}
return
}
val viewModel = viewModel<APModuleViewModel>()
LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
viewModel.fetchModuleList()
}
}
val webUILauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { viewModel.fetchModuleList() }
//TODO: FIXME -> val isSafeMode = Natives.getSafeMode()
val isSafeMode = false
val hasMagisk = hasMagisk()
val hideInstallButton = isSafeMode || hasMagisk
val moduleListState = rememberLazyListState()
Scaffold(
topBar = {
TopBar()
}, floatingActionButton = if (hideInstallButton) {
{ /* Empty */ }
} else {
{
val selectZipLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != RESULT_OK) {
return@rememberLauncherForActivityResult
}
val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult
Log.i("ModuleScreen", "select zip result: $uri")
navigator.navigate(InstallScreenDestination(uri, MODULE_TYPE.APM))
viewModel.markNeedRefresh()
}
FloatingActionButton(
contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
onClick = {
// select the zip file to install
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
selectZipLauncher.launch(intent)
}) {
Icon(
painter = painterResource(id = R.drawable.package_import),
contentDescription = null
)
}
}
}, snackbarHost = { SnackbarHost(snackBarHost) }) { innerPadding ->
when {
hasMagisk -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.apm_magisk_conflict),
textAlign = TextAlign.Center,
)
}
}
else -> {
ModuleList(
navigator,
viewModel = viewModel,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = moduleListState,
onInstallModule = {
navigator.navigate(InstallScreenDestination(it, MODULE_TYPE.APM))
},
onClickModule = { id, name, hasWebUi ->
if (hasWebUi) {
webUILauncher.launch(
Intent(
context, WebUIActivity::class.java
).setData("apatch://webui/$id".toUri()).putExtra("id", id)
.putExtra("name", name)
)
}
},
snackBarHost = snackBarHost,
context = context
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ModuleList(
navigator: DestinationsNavigator,
viewModel: APModuleViewModel,
modifier: Modifier = Modifier,
state: LazyListState,
onInstallModule: (Uri) -> Unit,
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit,
snackBarHost: SnackbarHostState,
context: Context
) {
val failedEnable = stringResource(R.string.apm_failed_to_enable)
val failedDisable = stringResource(R.string.apm_failed_to_disable)
val failedUninstall = stringResource(R.string.apm_uninstall_failed)
val successUninstall = stringResource(R.string.apm_uninstall_success)
val reboot = stringResource(id = R.string.reboot)
val rebootToApply = stringResource(id = R.string.apm_reboot_to_apply)
val moduleStr = stringResource(id = R.string.apm)
val uninstall = stringResource(id = R.string.apm_remove)
val cancel = stringResource(id = android.R.string.cancel)
val moduleUninstallConfirm = stringResource(id = R.string.apm_uninstall_confirm)
val updateText = stringResource(R.string.apm_update)
val changelogText = stringResource(R.string.apm_changelog)
val downloadingText = stringResource(R.string.apm_downloading)
val startDownloadingText = stringResource(R.string.apm_start_downloading)
val loadingDialog = rememberLoadingDialog()
val confirmDialog = rememberConfirmDialog()
suspend fun onModuleUpdate(
module: APModuleViewModel.ModuleInfo,
changelogUrl: String,
downloadUrl: String,
fileName: String
) {
val changelog = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
if (Patterns.WEB_URL.matcher(changelogUrl).matches()) {
apApp.okhttpClient.newCall(
okhttp3.Request.Builder().url(changelogUrl).build()
).execute().body!!.string()
} else {
changelogUrl
}
}
}
if (changelog.isNotEmpty()) {
// changelog is not empty, show it and wait for confirm
val confirmResult = confirmDialog.awaitConfirm(
changelogText,
content = changelog,
markdown = true,
confirm = updateText,
)
if (confirmResult != ConfirmResult.Confirmed) {
return
}
}
withContext(Dispatchers.Main) {
Toast.makeText(
context, startDownloadingText.format(module.name), Toast.LENGTH_SHORT
).show()
}
val downloading = downloadingText.format(module.name)
withContext(Dispatchers.IO) {
download(
context,
downloadUrl,
fileName,
downloading,
onDownloaded = onInstallModule,
onDownloading = {
launch(Dispatchers.Main) {
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
}
})
}
}
suspend fun onModuleUninstall(module: APModuleViewModel.ModuleInfo) {
val confirmResult = confirmDialog.awaitConfirm(
moduleStr,
content = moduleUninstallConfirm.format(module.name),
confirm = uninstall,
dismiss = cancel
)
if (confirmResult != ConfirmResult.Confirmed) {
return
}
val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
uninstallModule(module.id)
}
}
if (success) {
viewModel.fetchModuleList()
}
val message = if (success) {
successUninstall.format(module.name)
} else {
failedUninstall.format(module.name)
}
val actionLabel = if (success) {
reboot
} else {
null
}
val result = snackBarHost.showSnackbar(
message = message, actionLabel = actionLabel, duration = SnackbarDuration.Long
)
if (result == SnackbarResult.ActionPerformed) {
reboot()
}
}
PullToRefreshBox(
modifier = modifier,
onRefresh = { viewModel.fetchModuleList() },
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = state,
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = remember {
PaddingValues(
start = 16.dp,
top = 16.dp,
end = 16.dp,
bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */
)
},
) {
when {
viewModel.moduleList.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.apm_empty), textAlign = TextAlign.Center
)
}
}
}
else -> {
items(viewModel.moduleList) { module ->
var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) }
val scope = rememberCoroutineScope()
val updatedModule by produceState(initialValue = Triple("", "", "")) {
scope.launch(Dispatchers.IO) {
value = viewModel.checkUpdate(module)
}
}
ModuleItem(
navigator,
module,
isChecked,
updatedModule.first,
onUninstall = {
scope.launch { onModuleUninstall(module) }
},
onCheckChanged = {
scope.launch {
val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
toggleModule(module.id, !isChecked)
}
}
if (success) {
isChecked = it
viewModel.fetchModuleList()
val result = snackBarHost.showSnackbar(
message = rebootToApply,
actionLabel = reboot,
duration = SnackbarDuration.Long
)
if (result == SnackbarResult.ActionPerformed) {
reboot()
}
} else {
val message = if (isChecked) failedDisable else failedEnable
snackBarHost.showSnackbar(message.format(module.name))
}
}
},
onUpdate = {
scope.launch {
onModuleUpdate(
module,
updatedModule.third,
updatedModule.first,
"${module.name}-${updatedModule.second}.zip"
)
}
},
onClick = {
onClickModule(it.id, it.name, it.hasWebUi)
})
// fix last item shadow incomplete in LazyColumn
Spacer(Modifier.height(1.dp))
}
}
}
}
DownloadListener(context, onInstallModule)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar() {
TopAppBar(title = { Text(stringResource(R.string.apm)) })
}
@Composable
private fun ModuleItem(
navigator: DestinationsNavigator,
module: APModuleViewModel.ModuleInfo,
isChecked: Boolean,
updateUrl: String,
onUninstall: (APModuleViewModel.ModuleInfo) -> Unit,
onCheckChanged: (Boolean) -> Unit,
onUpdate: (APModuleViewModel.ModuleInfo) -> Unit,
onClick: (APModuleViewModel.ModuleInfo) -> Unit,
modifier: Modifier = Modifier,
alpha: Float = 1f,
) {
val decoration = if (!module.remove) TextDecoration.None else TextDecoration.LineThrough
val moduleAuthor = stringResource(id = R.string.apm_author)
val viewModel = viewModel<APModuleViewModel>()
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp,
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick(module) },
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.alpha(alpha = alpha)
.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = module.name,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
maxLines = 2,
textDecoration = decoration,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${module.version}, $moduleAuthor ${module.author}",
style = MaterialTheme.typography.bodySmall,
textDecoration = decoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
enabled = !module.update,
checked = isChecked,
onCheckedChange = onCheckChanged
)
}
Text(
modifier = Modifier
.alpha(alpha = alpha)
.padding(horizontal = 16.dp),
text = module.description,
style = MaterialTheme.typography.bodySmall,
textDecoration = decoration,
color = MaterialTheme.colorScheme.outline
)
HorizontalDivider(
thickness = 1.5.dp,
color = MaterialTheme.colorScheme.surface,
modifier = Modifier.padding(top = 8.dp)
)
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.weight(1f))
if (updateUrl.isNotEmpty()) {
ModuleUpdateButton(onClick = { onUpdate(module) })
Spacer(modifier = Modifier.width(12.dp))
}
if (module.hasWebUi) {
FilledTonalButton(
onClick = { onClick(module) },
enabled = true,
contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.settings),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.apm_webui_open),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
Spacer(modifier = Modifier.width(12.dp))
}
if (module.hasActionScript) {
FilledTonalButton(
onClick = {
navigator.navigate(ExecuteAPMActionScreenDestination(module.id))
viewModel.markNeedRefresh()
}, enabled = true, contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.settings),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.apm_action),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
Spacer(modifier = Modifier.width(12.dp))
}
ModuleRemoveButton(enabled = !module.remove, onClick = { onUninstall(module) })
}
}
if (module.remove) {
ModuleStateIndicator(R.drawable.trash)
}
if (module.update) {
ModuleStateIndicator(R.drawable.device_mobile_down)
}
}
}
}

View file

@ -0,0 +1,193 @@
package me.bmax.apatch.ui.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.R
import me.bmax.apatch.util.Version
@Destination<RootGraph>
@Composable
fun AboutScreen(navigator: DestinationsNavigator) {
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
TopBar(onBack = dropUnlessResumed { navigator.popBackStack() })
}
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier.size(95.dp),
color = colorResource(id = R.color.ic_launcher_background),
shape = CircleShape
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "icon",
modifier = Modifier.scale(1.4f)
)
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
Text(
text = stringResource(
id = R.string.about_app_version,
if (BuildConfig.VERSION_NAME.contains(BuildConfig.VERSION_CODE.toString())) "${BuildConfig.VERSION_CODE}" else "${BuildConfig.VERSION_CODE} (${BuildConfig.VERSION_NAME})"
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 5.dp)
)
Text(
text = stringResource(
id = R.string.about_powered_by,
"KernelPatch (${Version.buildKPVString()})"
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 5.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilledTonalButton(
onClick = { uriHandler.openUri("https://github.com/bmax121/APatch") }
) {
Icon(
painter = painterResource(id = R.drawable.github),
contentDescription = null
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.about_github))
}
FilledTonalButton(
onClick = { uriHandler.openUri("https://t.me/APatchChannel") }
) {
Icon(
painter = painterResource(id = R.drawable.telegram),
contentDescription = null
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.about_telegram_channel))
}
}
Row(
modifier = Modifier.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilledTonalButton(
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/APatch") }
) {
Icon(
painter = painterResource(id = R.drawable.weblate),
contentDescription = null,
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.about_weblate))
}
FilledTonalButton(
onClick = { uriHandler.openUri("https://t.me/apatch_discuss") }
) {
Icon(
painter = painterResource(id = R.drawable.telegram),
contentDescription = null
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.about_telegram_group))
}
}
OutlinedCard(
modifier = Modifier.padding(vertical = 30.dp, horizontal = 20.dp),
shape = RoundedCornerShape(15.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(all = 12.dp)
) {
Text(
text = stringResource(id = R.string.about_app_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.about)) },
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
},
)
}

View file

@ -0,0 +1,72 @@
package me.bmax.apatch.ui.screen
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.ui.graphics.vector.ImageVector
import com.ramcosta.composedestinations.generated.destinations.APModuleScreenDestination
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
import com.ramcosta.composedestinations.generated.destinations.KPModuleScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
import me.bmax.apatch.R
enum class BottomBarDestination(
val direction: DirectionDestinationSpec,
@param:StringRes val label: Int,
val iconSelected: ImageVector,
val iconNotSelected: ImageVector,
val kPatchRequired: Boolean,
val aPatchRequired: Boolean,
) {
Home(
HomeScreenDestination,
R.string.home,
Icons.Filled.Home,
Icons.Outlined.Home,
false,
false
),
KModule(
KPModuleScreenDestination,
R.string.kpm,
Icons.Filled.Build,
Icons.Outlined.Build,
true,
false
),
SuperUser(
SuperUserScreenDestination,
R.string.su_title,
Icons.Filled.Security,
Icons.Outlined.Security,
true,
false
),
AModule(
APModuleScreenDestination,
R.string.apm,
Icons.Filled.Apps,
Icons.Outlined.Apps,
false,
true
),
Settings(
SettingScreenDestination,
R.string.settings,
Icons.Filled.Settings,
Icons.Outlined.Settings,
false,
false
)
}

View file

@ -0,0 +1,152 @@
package me.bmax.apatch.ui.screen
import android.os.Environment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.R
import me.bmax.apatch.ui.component.KeyEventBlocker
import me.bmax.apatch.util.runAPModuleAction
import me.bmax.apatch.util.ui.LocalSnackbarHost
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
@Destination<RootGraph>
fun ExecuteAPMActionScreen(navigator: DestinationsNavigator, moduleId: String) {
var text by rememberSaveable { mutableStateOf("") }
var tempText: String
val logContent = rememberSaveable { StringBuilder() }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var actionResult: Boolean
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
return@LaunchedEffect
}
withContext(Dispatchers.IO) {
runAPModuleAction(
moduleId,
onStdout = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
},
onStderr = {
logContent.append(it).append("\n")
}
).let {
actionResult = it
}
}
if (actionResult) {
navigator.popBackStack()
}
}
Scaffold(
topBar = {
TopBar(
onBack = dropUnlessResumed {
navigator.popBackStack()
},
onSave = {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"APatch_apm_action_log_${date}.log"
)
file.writeText(logContent.toString())
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
}
}
)
},
snackbarHost = { SnackbarHost(snackBarHost) }
) { innerPadding ->
KeyEventBlocker {
it.key == Key.VolumeDown || it.key == Key.VolumeUp
}
Column(
modifier = Modifier
.fillMaxSize(1f)
.padding(innerPadding)
.verticalScroll(scrollState),
) {
LaunchedEffect(text) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Text(
modifier = Modifier.padding(8.dp),
text = text,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = FontFamily.Monospace,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.apm_action)) },
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
},
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = "Save log"
)
}
}
)
}

View file

@ -0,0 +1,996 @@
package me.bmax.apatch.ui.screen
import android.os.Build
import android.system.Os
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Cached
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.InstallMobile
import androidx.compose.material.icons.outlined.SystemUpdate
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.SecureFlagPolicy
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination
import com.ramcosta.composedestinations.generated.destinations.InstallModeSelectScreenDestination
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.bmax.apatch.APApplication
import me.bmax.apatch.Natives
import me.bmax.apatch.R
import me.bmax.apatch.apApp
import me.bmax.apatch.ui.component.ProvideMenuShape
import me.bmax.apatch.ui.component.rememberConfirmDialog
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
import me.bmax.apatch.util.LatestVersionInfo
import me.bmax.apatch.util.Version
import me.bmax.apatch.util.Version.getManagerVersion
import me.bmax.apatch.util.checkNewVersion
import me.bmax.apatch.util.getSELinuxStatus
import me.bmax.apatch.util.reboot
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
private val managerVersion = getManagerVersion()
@Destination<RootGraph>(start = true)
@Composable
fun HomeScreen(navigator: DestinationsNavigator) {
var showPatchFloatAction by remember { mutableStateOf(true) }
val kpState by APApplication.kpStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
val apState by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
if (kpState != APApplication.State.UNKNOWN_STATE) {
showPatchFloatAction = false
}
Scaffold(topBar = {
TopBar(onInstallClick = dropUnlessResumed {
navigator.navigate(InstallModeSelectScreenDestination)
}, navigator, kpState)
}) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(Modifier.height(0.dp))
WarningCard()
KStatusCard(kpState, apState, navigator)
if (kpState != APApplication.State.UNKNOWN_STATE && apState != APApplication.State.ANDROIDPATCH_INSTALLED) {
AStatusCard(apState)
}
val checkUpdate = APApplication.sharedPreferences.getBoolean("check_update", true)
if (checkUpdate) {
UpdateCard()
}
InfoCard(kpState, apState)
LearnMoreCard()
Spacer(Modifier)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UninstallDialog(showDialog: MutableState<Boolean>, navigator: DestinationsNavigator) {
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
)
) {
Surface(
modifier = Modifier
.width(320.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(20.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.CenterHorizontally)
) {
Text(
text = stringResource(id = R.string.home_dialog_uninstall_title),
style = MaterialTheme.typography.headlineSmall
)
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center
) {
TextButton(onClick = {
showDialog.value = false
APApplication.uninstallApatch()
}) {
Text(text = stringResource(id = R.string.home_dialog_uninstall_ap_only))
}
TextButton(onClick = {
showDialog.value = false
APApplication.uninstallApatch()
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.UNPATCH))
}) {
Text(text = stringResource(id = R.string.home_dialog_uninstall_all))
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthFailedTipDialog(showDialog: MutableState<Boolean>) {
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
securePolicy = SecureFlagPolicy.SecureOff
)
) {
Surface(
modifier = Modifier
.width(320.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(20.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
// Title
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.home_dialog_auth_fail_title),
style = MaterialTheme.typography.headlineSmall
)
}
// Content
Box(
Modifier
.weight(weight = 1f, fill = false)
.padding(PaddingValues(bottom = 24.dp))
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.home_dialog_auth_fail_content),
style = MaterialTheme.typography.bodyMedium
)
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDialog.value = false }) {
Text(text = stringResource(id = android.R.string.ok))
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
}
val checkSuperKeyValidation: (superKey: String) -> Boolean = { superKey ->
superKey.length in 8..63 && superKey.any { it.isDigit() } && superKey.any { it.isLetter() }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AuthSuperKey(showDialog: MutableState<Boolean>, showFailedDialog: MutableState<Boolean>) {
var key by remember { mutableStateOf("") }
var keyVisible by remember { mutableStateOf(false) }
var enable by remember { mutableStateOf(false) }
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
securePolicy = SecureFlagPolicy.SecureOff
)
) {
Surface(
modifier = Modifier
.width(310.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(30.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
// Title
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.home_auth_key_title),
style = MaterialTheme.typography.headlineSmall
)
}
// Content
Box(
Modifier
.weight(weight = 1f, fill = false)
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.home_auth_key_desc),
style = MaterialTheme.typography.bodyMedium
)
}
// Content2
Box(
contentAlignment = Alignment.CenterEnd,
) {
OutlinedTextField(
value = key,
modifier = Modifier
.fillMaxWidth()
.padding(top = 6.dp),
onValueChange = {
key = it
enable = checkSuperKeyValidation(key)
},
shape = RoundedCornerShape(50.0f),
label = { Text(stringResource(id = R.string.super_key)) },
visualTransformation = if (keyVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
IconButton(
modifier = Modifier
.size(40.dp)
.padding(top = 15.dp, end = 5.dp),
onClick = { keyVisible = !keyVisible }) {
Icon(
imageVector = if (keyVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = null,
tint = Color.Gray
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Buttons
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDialog.value = false }) {
Text(stringResource(id = android.R.string.cancel))
}
Button(onClick = {
showDialog.value = false
val preVerifyKey = Natives.nativeReady(key)
if (preVerifyKey) {
APApplication.superKey = key
} else {
showFailedDialog.value = true
}
}, enabled = enable) {
Text(stringResource(id = android.R.string.ok))
}
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
@Composable
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem(text = {
Text(stringResource(id))
}, onClick = {
reboot(reason)
})
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
onInstallClick: () -> Unit, navigator: DestinationsNavigator, kpState: APApplication.State
) {
val uriHandler = LocalUriHandler.current
var showDropdownMoreOptions by remember { mutableStateOf(false) }
var showDropdownReboot by remember { mutableStateOf(false) }
TopAppBar(title = {
Text(stringResource(R.string.app_name))
}, actions = {
IconButton(onClick = onInstallClick) {
Icon(
imageVector = Icons.Filled.InstallMobile,
contentDescription = stringResource(id = R.string.mode_select_page_title)
)
}
if (kpState != APApplication.State.UNKNOWN_STATE) {
IconButton(onClick = {
showDropdownReboot = true
}) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(id = R.string.reboot)
)
ProvideMenuShape(RoundedCornerShape(10.dp)) {
DropdownMenu(expanded = showDropdownReboot, onDismissRequest = {
showDropdownReboot = false
}) {
RebootDropdownItem(id = R.string.reboot)
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")
}
}
}
}
Box {
IconButton(onClick = { showDropdownMoreOptions = true }) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings)
)
ProvideMenuShape(RoundedCornerShape(10.dp)) {
DropdownMenu(expanded = showDropdownMoreOptions, onDismissRequest = {
showDropdownMoreOptions = false
}) {
DropdownMenuItem(text = {
Text(stringResource(R.string.home_more_menu_feedback_or_suggestion))
}, onClick = {
showDropdownMoreOptions = false
uriHandler.openUri("https://github.com/bmax121/APatch/issues/new/choose")
})
DropdownMenuItem(text = {
Text(stringResource(R.string.home_more_menu_about))
}, onClick = {
navigator.navigate(AboutScreenDestination)
showDropdownMoreOptions = false
})
}
}
}
}
})
}
@Composable
private fun KStatusCard(
kpState: APApplication.State, apState: APApplication.State, navigator: DestinationsNavigator
) {
val showAuthFailedTipDialog = remember { mutableStateOf(false) }
if (showAuthFailedTipDialog.value) {
AuthFailedTipDialog(showDialog = showAuthFailedTipDialog)
}
val showAuthKeyDialog = remember { mutableStateOf(false) }
if (showAuthKeyDialog.value) {
AuthSuperKey(showDialog = showAuthKeyDialog, showFailedDialog = showAuthFailedTipDialog)
}
val showUninstallDialog = remember { mutableStateOf(false) }
if (showUninstallDialog.value) {
UninstallDialog(showDialog = showUninstallDialog, navigator)
}
val cardBackgroundColor = when (kpState) {
APApplication.State.KERNELPATCH_INSTALLED -> {
MaterialTheme.colorScheme.primary
}
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
MaterialTheme.colorScheme.secondary
}
else -> {
MaterialTheme.colorScheme.secondaryContainer
}
}
ElevatedCard(
onClick = {
if (kpState != APApplication.State.KERNELPATCH_INSTALLED) {
navigator.navigate(InstallModeSelectScreenDestination)
}
},
colors = CardDefaults.elevatedCardColors(containerColor = cardBackgroundColor),
elevation = CardDefaults.cardElevation(
defaultElevation = if (kpState == APApplication.State.UNKNOWN_STATE) 0.dp else 6.dp
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (kpState == APApplication.State.KERNELPATCH_NEED_UPDATE) {
Row {
Text(
text = stringResource(R.string.kernel_patch),
style = MaterialTheme.typography.titleMedium
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
when (kpState) {
APApplication.State.KERNELPATCH_INSTALLED -> {
Icon(Icons.Filled.CheckCircle, stringResource(R.string.home_working))
}
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
Icon(Icons.Outlined.SystemUpdate, stringResource(R.string.home_need_update))
}
else -> {
Icon(Icons.AutoMirrored.Outlined.HelpOutline, "Unknown")
}
}
Column(
Modifier
.weight(2f)
.padding(start = 16.dp)
) {
when (kpState) {
APApplication.State.KERNELPATCH_INSTALLED -> {
Text(
text = stringResource(R.string.home_working),
style = MaterialTheme.typography.titleMedium
)
}
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
Text(
text = stringResource(R.string.home_need_update),
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(6.dp))
Text(
text = stringResource(
R.string.kpatch_version_update,
Version.installedKPVString(),
Version.buildKPVString()
), style = MaterialTheme.typography.bodyMedium
)
}
else -> {
Text(
text = stringResource(R.string.home_install_unknown),
style = MaterialTheme.typography.titleMedium
)
Text(
text = stringResource(R.string.home_install_unknown_summary),
style = MaterialTheme.typography.bodyMedium
)
}
}
if (kpState != APApplication.State.UNKNOWN_STATE && kpState != APApplication.State.KERNELPATCH_NEED_UPDATE && kpState != APApplication.State.KERNELPATCH_NEED_REBOOT) {
Spacer(Modifier.height(4.dp))
Text(
text = "${Version.installedKPVString()} (${managerVersion.second}) - " + if (apState != APApplication.State.ANDROIDPATCH_NOT_INSTALLED) "Full" else "KernelPatch",
style = MaterialTheme.typography.bodyMedium
)
}
}
Column(
modifier = Modifier.align(Alignment.CenterVertically)
) {
Button(onClick = {
when (kpState) {
APApplication.State.UNKNOWN_STATE -> {
showAuthKeyDialog.value = true
}
APApplication.State.KERNELPATCH_NEED_UPDATE -> {
// todo: remove legacy compact for kp < 0.9.0
if (Version.installedKPVUInt() < 0x900u) {
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_ONLY))
} else {
navigator.navigate(InstallModeSelectScreenDestination)
}
}
APApplication.State.KERNELPATCH_NEED_REBOOT -> {
reboot()
}
APApplication.State.KERNELPATCH_UNINSTALLING -> {
// Do nothing
}
else -> {
if (apState == APApplication.State.ANDROIDPATCH_INSTALLED || apState == APApplication.State.ANDROIDPATCH_NEED_UPDATE) {
showUninstallDialog.value = true
} else {
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.UNPATCH))
}
}
}
}, content = {
when (kpState) {
APApplication.State.UNKNOWN_STATE -> {
Text(text = stringResource(id = R.string.super_key))
}
APApplication.State.KERNELPATCH_NEED_UPDATE -> {
Text(text = stringResource(id = R.string.home_ap_cando_update))
}
APApplication.State.KERNELPATCH_NEED_REBOOT -> {
Text(text = stringResource(id = R.string.home_ap_cando_reboot))
}
APApplication.State.KERNELPATCH_UNINSTALLING -> {
Icon(Icons.Outlined.Cached, contentDescription = "busy")
}
else -> {
Text(text = stringResource(id = R.string.home_ap_cando_uninstall))
}
}
})
}
}
}
}
}
@Composable
private fun AStatusCard(apState: APApplication.State) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Text(
text = stringResource(R.string.android_patch),
style = MaterialTheme.typography.titleMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
when (apState) {
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
Icon(Icons.Outlined.Block, stringResource(R.string.home_not_installed))
}
APApplication.State.ANDROIDPATCH_INSTALLING -> {
Icon(Icons.Outlined.InstallMobile, stringResource(R.string.home_installing))
}
APApplication.State.ANDROIDPATCH_INSTALLED -> {
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
}
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
Icon(Icons.Outlined.SystemUpdate, stringResource(R.string.home_need_update))
}
else -> {
Icon(
Icons.AutoMirrored.Outlined.HelpOutline,
stringResource(R.string.home_install_unknown)
)
}
}
Column(
Modifier
.weight(2f)
.padding(start = 16.dp)
) {
when (apState) {
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
Text(
text = stringResource(R.string.home_not_installed),
style = MaterialTheme.typography.titleMedium
)
}
APApplication.State.ANDROIDPATCH_INSTALLING -> {
Text(
text = stringResource(R.string.home_installing),
style = MaterialTheme.typography.titleMedium
)
}
APApplication.State.ANDROIDPATCH_INSTALLED -> {
Text(
text = stringResource(R.string.home_working),
style = MaterialTheme.typography.titleMedium
)
}
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
Text(
text = stringResource(R.string.home_need_update),
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(6.dp))
Text(
text = stringResource(
R.string.apatch_version_update,
Version.installedApdVString,
managerVersion.second
), style = MaterialTheme.typography.bodyMedium
)
}
else -> {
Text(
text = stringResource(R.string.home_install_unknown),
style = MaterialTheme.typography.titleMedium
)
}
}
}
if (apState != APApplication.State.UNKNOWN_STATE) {
Column(
modifier = Modifier.align(Alignment.CenterVertically)
) {
Button(onClick = {
when (apState) {
APApplication.State.ANDROIDPATCH_NOT_INSTALLED, APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
APApplication.installApatch()
}
APApplication.State.ANDROIDPATCH_UNINSTALLING -> {
// Do nothing
}
else -> {
APApplication.uninstallApatch()
}
}
}, content = {
when (apState) {
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
Text(text = stringResource(id = R.string.home_ap_cando_install))
}
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
Text(text = stringResource(id = R.string.home_ap_cando_update))
}
APApplication.State.ANDROIDPATCH_UNINSTALLING -> {
Icon(Icons.Outlined.Cached, contentDescription = "busy")
}
else -> {
Text(text = stringResource(id = R.string.home_ap_cando_uninstall))
}
}
})
}
}
}
}
}
}
@Composable
fun WarningCard() {
var show by rememberSaveable { mutableStateOf(apApp.getBackupWarningState()) }
if (show) {
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 6.dp
), colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.error
})
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(Icons.Filled.Warning, contentDescription = "warning")
}
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.patch_warnning),
)
Spacer(Modifier.width(12.dp))
Icon(
Icons.Outlined.Clear,
contentDescription = "",
modifier = Modifier.clickable {
show = false
apApp.updateBackupWarningState(false)
},
)
}
}
}
}
}
}
private fun getSystemVersion(): String {
return "${Build.VERSION.RELEASE} ${if (Build.VERSION.PREVIEW_SDK_INT != 0) "Preview" else ""} (API ${Build.VERSION.SDK_INT})"
}
private fun getDeviceInfo(): String {
var manufacturer =
Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)
if (!Build.BRAND.equals(Build.MANUFACTURER, ignoreCase = true)) {
manufacturer += " " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)
}
manufacturer += " " + Build.MODEL + " "
return manufacturer
}
@Composable
private fun InfoCard(kpState: APApplication.State, apState: APApplication.State) {
ElevatedCard {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
) {
val contents = StringBuilder()
val uname = Os.uname()
@Composable
fun InfoCardItem(label: String, content: String) {
contents.appendLine(label).appendLine(content).appendLine()
Text(text = label, style = MaterialTheme.typography.bodyLarge)
Text(text = content, style = MaterialTheme.typography.bodyMedium)
}
if (kpState != APApplication.State.UNKNOWN_STATE) {
InfoCardItem(
stringResource(R.string.home_kpatch_version), Version.installedKPVString()
)
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_su_path), Natives.suPath())
Spacer(Modifier.height(16.dp))
}
if (apState != APApplication.State.UNKNOWN_STATE && apState != APApplication.State.ANDROIDPATCH_NOT_INSTALLED) {
InfoCardItem(
stringResource(R.string.home_apatch_version), managerVersion.second.toString()
)
Spacer(Modifier.height(16.dp))
}
InfoCardItem(stringResource(R.string.home_device_info), getDeviceInfo())
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_system_version), getSystemVersion())
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_fingerprint), Build.FINGERPRINT)
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
}
}
}
@Composable
fun WarningCard(
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(
containerColor = color
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.padding(24.dp)) {
Text(
text = message, style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun UpdateCard() {
val latestVersionInfo = LatestVersionInfo()
val newVersion by produceState(initialValue = latestVersionInfo) {
value = withContext(Dispatchers.IO) {
checkNewVersion()
}
}
val currentVersionCode = managerVersion.second
val newVersionCode = newVersion.versionCode
val newVersionUrl = newVersion.downloadUrl
val changelog = newVersion.changelog
val uriHandler = LocalUriHandler.current
val title = stringResource(id = R.string.apm_changelog)
val updateText = stringResource(id = R.string.apm_update)
AnimatedVisibility(
visible = newVersionCode > currentVersionCode,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard(
message = stringResource(id = R.string.home_new_apatch_found).format(newVersionCode),
MaterialTheme.colorScheme.outlineVariant
) {
if (changelog.isEmpty()) {
uriHandler.openUri(newVersionUrl)
} else {
updateDialog.showConfirm(
title = title, content = changelog, markdown = true, confirm = updateText
)
}
}
}
}
@Composable
fun LearnMoreCard() {
val uriHandler = LocalUriHandler.current
ElevatedCard {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri("https://apatch.dev")
}
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
Column {
Text(
text = stringResource(R.string.home_learn_apatch),
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_learn_apatch),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}

View file

@ -0,0 +1,175 @@
package me.bmax.apatch.ui.screen
import android.net.Uri
import android.os.Environment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.R
import me.bmax.apatch.ui.component.KeyEventBlocker
import me.bmax.apatch.util.installModule
import me.bmax.apatch.util.reboot
import me.bmax.apatch.util.ui.LocalSnackbarHost
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
enum class MODULE_TYPE {
KPM, APM
}
@Composable
@Destination<RootGraph>
fun InstallScreen(navigator: DestinationsNavigator, uri: Uri, type: MODULE_TYPE) {
var text by remember { mutableStateOf("") }
var tempText: String
val logContent = remember { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
return@LaunchedEffect
}
withContext(Dispatchers.IO) {
installModule(uri, type, onFinish = { success ->
if (success) {
showFloatAction = true
}
}, onStdout = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
}, onStderr = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
})
}
}
Scaffold(topBar = {
TopBar(onBack = dropUnlessResumed {
navigator.popBackStack()
}, onSave = {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"APatch_install_${type}_log_${date}.log"
)
file.writeText(logContent.toString())
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
}
})
}, floatingActionButton = {
if (showFloatAction) {
val reboot = stringResource(id = R.string.reboot)
ExtendedFloatingActionButton(
onClick = {
scope.launch {
withContext(Dispatchers.IO) {
reboot()
}
}
},
icon = { Icon(Icons.Filled.Refresh, reboot) },
text = { Text(text = reboot) },
)
}
}, snackbarHost = { SnackbarHost(snackBarHost) }) { innerPadding ->
KeyEventBlocker {
it.key == Key.VolumeDown || it.key == Key.VolumeUp
}
Column(
modifier = Modifier
.fillMaxSize(1f)
.padding(innerPadding)
.verticalScroll(scrollState),
) {
LaunchedEffect(text) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Text(
modifier = Modifier.padding(8.dp),
text = text,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = FontFamily.Monospace,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
TopAppBar(title = { Text(stringResource(R.string.apm_install)) }, navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
}, actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save, contentDescription = "Localized description"
)
}
})
}
@Preview
@Composable
fun InstallPreview() {
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY)
}

View file

@ -0,0 +1,197 @@
package me.bmax.apatch.ui.screen
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import me.bmax.apatch.R
import me.bmax.apatch.ui.component.rememberConfirmDialog
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
import me.bmax.apatch.util.isABDevice
import me.bmax.apatch.util.rootAvailable
var selectedBootImage: Uri? = null
@Destination<RootGraph>
@Composable
fun InstallModeSelectScreen(navigator: DestinationsNavigator) {
var installMethod by remember {
mutableStateOf<InstallMethod?>(null)
}
Scaffold(topBar = {
TopBar(
onBack = dropUnlessResumed { navigator.popBackStack() },
)
}) {
Column(modifier = Modifier.padding(it)) {
SelectInstallMethod(
onSelected = { method ->
installMethod = method
},
navigator = navigator
)
}
}
}
sealed class InstallMethod {
data class SelectFile(
val uri: Uri? = null,
@param:StringRes override val label: Int = R.string.mode_select_page_select_file,
) : InstallMethod()
data object DirectInstall : InstallMethod() {
override val label: Int
get() = R.string.mode_select_page_patch_and_install
}
data object DirectInstallToInactiveSlot : InstallMethod() {
override val label: Int
get() = R.string.mode_select_page_install_inactive_slot
}
abstract val label: Int
open val summary: String? = null
}
@Composable
private fun SelectInstallMethod(
onSelected: (InstallMethod) -> Unit = {},
navigator: DestinationsNavigator
) {
val rootAvailable = rootAvailable()
val isAbDevice = isABDevice()
val radioOptions =
mutableListOf<InstallMethod>(InstallMethod.SelectFile())
if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall)
if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
}
}
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
val option = InstallMethod.SelectFile(uri)
selectedOption = option
onSelected(option)
selectedBootImage = option.uri
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_ONLY))
}
}
}
val confirmDialog = rememberConfirmDialog(onConfirm = {
selectedOption = InstallMethod.DirectInstallToInactiveSlot
onSelected(InstallMethod.DirectInstallToInactiveSlot)
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.INSTALL_TO_NEXT_SLOT))
}, onDismiss = null)
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.mode_select_page_install_inactive_slot_warning)
val onClick = { option: InstallMethod ->
when (option) {
is InstallMethod.SelectFile -> {
// Reset before selecting
selectedBootImage = null
selectImageLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
}
)
}
is InstallMethod.DirectInstall -> {
selectedOption = option
onSelected(option)
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_AND_INSTALL))
}
is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent)
}
}
}
Column {
radioOptions.forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
onClick(option)
}) {
RadioButton(selected = option.javaClass == selectedOption?.javaClass, onClick = {
onClick(option)
})
Column {
Text(
text = stringResource(id = option.label),
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
)
option.summary?.let {
Text(
text = it,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.mode_select_page_title)) },
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
},
)
}

View file

@ -0,0 +1,607 @@
package me.bmax.apatch.ui.screen
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.APApplication
import me.bmax.apatch.Natives
import me.bmax.apatch.R
import me.bmax.apatch.apApp
import me.bmax.apatch.ui.component.ConfirmResult
import me.bmax.apatch.ui.component.KPModuleRemoveButton
import me.bmax.apatch.ui.component.LoadingDialogHandle
import me.bmax.apatch.ui.component.ProvideMenuShape
import me.bmax.apatch.ui.component.rememberConfirmDialog
import me.bmax.apatch.ui.component.rememberLoadingDialog
import me.bmax.apatch.ui.viewmodel.KPModel
import me.bmax.apatch.ui.viewmodel.KPModuleViewModel
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
import me.bmax.apatch.util.inputStream
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
import me.bmax.apatch.util.writeTo
import java.io.IOException
private const val TAG = "KernelPatchModule"
private lateinit var targetKPMToControl: KPModel.KPMInfo
@Destination<RootGraph>
@Composable
fun KPModuleScreen(navigator: DestinationsNavigator) {
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
if (state == APApplication.State.UNKNOWN_STATE) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Text(
text = stringResource(id = R.string.kpm_kp_not_installed),
style = MaterialTheme.typography.titleMedium
)
}
}
return
}
val viewModel = viewModel<KPModuleViewModel>()
LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
viewModel.fetchModuleList()
}
}
val kpModuleListState = rememberLazyListState()
Scaffold(topBar = {
TopBar()
}, floatingActionButton = run {
{
val scope = rememberCoroutineScope()
val context = LocalContext.current
val moduleLoad = stringResource(id = R.string.kpm_load)
val moduleInstall = stringResource(id = R.string.kpm_install)
val moduleEmbed = stringResource(id = R.string.kpm_embed)
val successToastText = stringResource(id = R.string.kpm_load_toast_succ)
val failToastText = stringResource(id = R.string.kpm_load_toast_failed)
val loadingDialog = rememberLoadingDialog()
val selectZipLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != RESULT_OK) {
return@rememberLauncherForActivityResult
}
val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult
Log.i(TAG, "select zip result: $uri")
navigator.navigate(InstallScreenDestination(uri, MODULE_TYPE.KPM))
}
val selectKpmLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != RESULT_OK) {
return@rememberLauncherForActivityResult
}
val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult
// todo: args
scope.launch {
val rc = loadModule(loadingDialog, uri, "") == 0
val toastText = if (rc) successToastText else failToastText
withContext(Dispatchers.Main) {
Toast.makeText(
context, toastText, Toast.LENGTH_SHORT
).show()
}
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
}
}
var expanded by remember { mutableStateOf(false) }
val options = listOf(moduleEmbed, moduleInstall, moduleLoad)
Column {
FloatingActionButton(
onClick = {
expanded = !expanded
},
contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
painter = painterResource(id = R.drawable.package_import),
contentDescription = null
)
}
ProvideMenuShape(RoundedCornerShape(10.dp)) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
properties = PopupProperties(focusable = true)
) {
options.forEach { label ->
DropdownMenuItem(text = { Text(label) }, onClick = {
expanded = false
when (label) {
moduleEmbed -> {
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_AND_INSTALL))
}
moduleInstall -> {
// val intent = Intent(Intent.ACTION_GET_CONTENT)
// intent.type = "application/zip"
// selectZipLauncher.launch(intent)
Toast.makeText(
context,
"Under development",
Toast.LENGTH_SHORT
).show()
}
moduleLoad -> {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
selectKpmLauncher.launch(intent)
}
}
})
}
}
}
}
}
}) { innerPadding ->
KPModuleList(
viewModel = viewModel,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = kpModuleListState
)
}
}
suspend fun loadModule(loadingDialog: LoadingDialogHandle, uri: Uri, args: String): Int {
val rc = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
run {
val kpmDir: ExtendedFile =
FileSystemManager.getLocal().getFile(apApp.filesDir.parent, "kpm")
kpmDir.deleteRecursively()
kpmDir.mkdirs()
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
val kpm = kpmDir.getChildFile("${rand}.kpm")
Log.d(TAG, "save tmp kpm: ${kpm.path}")
var rc = -1
try {
uri.inputStream().buffered().writeTo(kpm)
rc = Natives.loadKernelPatchModule(kpm.path, args).toInt()
} catch (e: IOException) {
Log.e(TAG, "Copy kpm error: $e")
}
Log.d(TAG, "load ${kpm.path} rc: $rc")
rc
}
}
}
return rc
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KPMControlDialog(showDialog: MutableState<Boolean>) {
var controlParam by remember { mutableStateOf("") }
var enable by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val loadingDialog = rememberLoadingDialog()
val context = LocalContext.current
val outMsgStringRes = stringResource(id = R.string.kpm_control_outMsg)
val okStringRes = stringResource(id = R.string.kpm_control_ok)
val failedStringRes = stringResource(id = R.string.kpm_control_failed)
lateinit var controlResult: Natives.KPMCtlRes
suspend fun onModuleControl(module: KPModel.KPMInfo) {
loadingDialog.withLoading {
withContext(Dispatchers.IO) {
controlResult = Natives.kernelPatchModuleControl(module.name, controlParam)
}
}
if (controlResult.rc >= 0) {
Toast.makeText(
context,
"$okStringRes\n${outMsgStringRes}: ${controlResult.outMsg}",
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
"$failedStringRes\n${outMsgStringRes}: ${controlResult.outMsg}",
Toast.LENGTH_SHORT
).show()
}
}
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
)
) {
Surface(
modifier = Modifier
.width(310.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(30.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.kpm_control_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
}
Box(
Modifier
.weight(weight = 1f, fill = false)
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.kpm_control_dialog_content),
style = MaterialTheme.typography.bodyMedium
)
}
Box(
contentAlignment = Alignment.CenterEnd,
) {
OutlinedTextField(
value = controlParam,
modifier = Modifier
.fillMaxWidth()
.padding(top = 6.dp),
onValueChange = {
controlParam = it
enable = controlParam.isNotBlank()
},
shape = RoundedCornerShape(50.0f),
label = { Text(stringResource(id = R.string.kpm_control_paramters)) },
visualTransformation = VisualTransformation.None,
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDialog.value = false }) {
Text(stringResource(id = android.R.string.cancel))
}
Button(onClick = {
showDialog.value = false
scope.launch { onModuleControl(targetKPMToControl) }
}, enabled = enable) {
Text(stringResource(id = android.R.string.ok))
}
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun KPModuleList(
viewModel: KPModuleViewModel, modifier: Modifier = Modifier, state: LazyListState
) {
val moduleStr = stringResource(id = R.string.kpm)
val moduleUninstallConfirm = stringResource(id = R.string.kpm_unload_confirm)
val uninstall = stringResource(id = R.string.kpm_unload)
val cancel = stringResource(id = android.R.string.cancel)
val confirmDialog = rememberConfirmDialog()
val loadingDialog = rememberLoadingDialog()
val showKPMControlDialog = remember { mutableStateOf(false) }
if (showKPMControlDialog.value) {
KPMControlDialog(showDialog = showKPMControlDialog)
}
suspend fun onModuleUninstall(module: KPModel.KPMInfo) {
val confirmResult = confirmDialog.awaitConfirm(
moduleStr,
content = moduleUninstallConfirm.format(module.name),
confirm = uninstall,
dismiss = cancel
)
if (confirmResult != ConfirmResult.Confirmed) {
return
}
val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
Natives.unloadKernelPatchModule(module.name) == 0L
}
}
if (success) {
viewModel.fetchModuleList()
}
}
PullToRefreshBox(
modifier = modifier,
onRefresh = { viewModel.fetchModuleList() },
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = state,
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = remember {
PaddingValues(
start = 16.dp,
top = 16.dp,
end = 16.dp,
bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */
)
},
) {
when {
viewModel.moduleList.isEmpty() -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.kpm_apm_empty), textAlign = TextAlign.Center
)
}
}
}
else -> {
items(viewModel.moduleList) { module ->
val scope = rememberCoroutineScope()
KPModuleItem(
module,
onUninstall = {
scope.launch { onModuleUninstall(module) }
},
onControl = {
targetKPMToControl = module
showKPMControlDialog.value = true
},
)
// fix last item shadow incomplete in LazyColumn
Spacer(Modifier.height(1.dp))
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar() {
TopAppBar(title = { Text(stringResource(R.string.kpm)) })
}
@Composable
private fun KPModuleItem(
module: KPModel.KPMInfo,
onUninstall: (KPModel.KPMInfo) -> Unit,
onControl: (KPModel.KPMInfo) -> Unit,
modifier: Modifier = Modifier,
alpha: Float = 1f,
) {
val moduleAuthor = stringResource(id = R.string.kpm_author)
val moduleArgs = stringResource(id = R.string.kpm_args)
val decoration = TextDecoration.None
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp,
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.alpha(alpha = alpha)
.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = module.name,
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
maxLines = 2,
textDecoration = decoration,
overflow = TextOverflow.Ellipsis
)
Text(
text = "${module.version}, $moduleAuthor ${module.author}",
style = MaterialTheme.typography.bodySmall,
textDecoration = decoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "$moduleArgs: ${module.args}",
style = MaterialTheme.typography.bodySmall,
textDecoration = decoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Text(
modifier = Modifier
.alpha(alpha = alpha)
.padding(horizontal = 16.dp),
text = module.description,
style = MaterialTheme.typography.bodySmall,
textDecoration = decoration,
color = MaterialTheme.colorScheme.outline
)
HorizontalDivider(
thickness = 1.5.dp,
color = MaterialTheme.colorScheme.surface,
modifier = Modifier.padding(top = 8.dp)
)
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.weight(1f))
FilledTonalButton(
onClick = { onControl(module) },
enabled = true,
contentPadding = PaddingValues(horizontal = 12.dp)
) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = R.drawable.settings),
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = stringResource(id = R.string.kpm_control),
maxLines = 1,
overflow = TextOverflow.Visible,
softWrap = false
)
}
Spacer(modifier = Modifier.width(12.dp))
KPModuleRemoveButton(enabled = true, onClick = { onUninstall(module) })
}
}
}
}
}

View file

@ -0,0 +1,624 @@
package me.bmax.apatch.ui.screen
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.R
import me.bmax.apatch.ui.viewmodel.KPModel
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
import me.bmax.apatch.util.Version
import me.bmax.apatch.util.reboot
private const val TAG = "Patches"
@Destination<RootGraph>
@Composable
fun Patches(mode: PatchesViewModel.PatchMode) {
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val viewModel = viewModel<PatchesViewModel>()
SideEffect {
viewModel.prepare(mode)
}
Scaffold(topBar = {
TopBar()
}, floatingActionButton = {
if (viewModel.needReboot) {
val reboot = stringResource(id = R.string.reboot)
ExtendedFloatingActionButton(
onClick = {
scope.launch {
withContext(Dispatchers.IO) {
reboot()
}
}
},
icon = { Icon(Icons.Filled.Refresh, reboot) },
text = { Text(text = reboot) },
)
}
}) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
val context = LocalContext.current
// request permissions
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
context as Activity,
permissionsToRequest.toTypedArray(),
1001
)
}
PatchMode(mode)
ErrorView(viewModel.error)
KernelPatchImageView(viewModel.kpimgInfo)
if (mode == PatchesViewModel.PatchMode.PATCH_ONLY && selectedBootImage != null && viewModel.kimgInfo.banner.isEmpty()) {
viewModel.copyAndParseBootimg(selectedBootImage!!)
// Fix endless loop. It's not normal if (parse done && working thread is not working) but banner still null
// Leave user re-choose
if (!viewModel.running && viewModel.kimgInfo.banner.isEmpty()) {
selectedBootImage = null
}
}
// select boot.img
if (mode == PatchesViewModel.PatchMode.PATCH_ONLY && viewModel.kimgInfo.banner.isEmpty()) {
SelectFileButton(
text = stringResource(id = R.string.patch_select_bootimg_btn),
onSelected = { data, uri ->
Log.d(TAG, "select boot.img, data: $data, uri: $uri")
viewModel.copyAndParseBootimg(uri)
}
)
}
if (viewModel.bootSlot.isNotEmpty() || viewModel.bootDev.isNotEmpty()) {
BootimgView(slot = viewModel.bootSlot, boot = viewModel.bootDev)
}
if (viewModel.kimgInfo.banner.isNotEmpty()) {
KernelImageView(viewModel.kimgInfo)
}
if (mode != PatchesViewModel.PatchMode.UNPATCH && viewModel.kimgInfo.banner.isNotEmpty()) {
SetSuperKeyView(viewModel)
}
// existed extras
if (mode == PatchesViewModel.PatchMode.PATCH_AND_INSTALL || mode == PatchesViewModel.PatchMode.INSTALL_TO_NEXT_SLOT) {
viewModel.existedExtras.forEach(action = {
ExtraItem(extra = it, true, onDelete = {
viewModel.existedExtras.remove(it)
})
})
}
// add new extras
if (mode != PatchesViewModel.PatchMode.UNPATCH) {
viewModel.newExtras.forEach(action = {
ExtraItem(extra = it, false, onDelete = {
val idx = viewModel.newExtras.indexOf(it)
viewModel.newExtras.remove(it)
viewModel.newExtrasFileName.removeAt(idx)
})
})
}
// add new KPM
if (viewModel.superkey.isNotEmpty() && !viewModel.patching && !viewModel.patchdone && mode != PatchesViewModel.PatchMode.UNPATCH) {
SelectFileButton(
text = stringResource(id = R.string.patch_embed_kpm_btn),
onSelected = { data, uri ->
Log.d(TAG, "select kpm, data: $data, uri: $uri")
viewModel.embedKPM(uri)
}
)
}
// do patch, update, unpatch
if (!viewModel.patching && !viewModel.patchdone) {
// patch start
if (mode != PatchesViewModel.PatchMode.UNPATCH && viewModel.superkey.isNotEmpty()) {
StartButton(stringResource(id = R.string.patch_start_patch_btn)) {
viewModel.doPatch(
mode
)
}
}
// unpatch
if (mode == PatchesViewModel.PatchMode.UNPATCH && viewModel.kimgInfo.banner.isNotEmpty()) {
StartButton(stringResource(id = R.string.patch_start_unpatch_btn)) { viewModel.doUnpatch() }
}
}
// patch log
if (viewModel.patching || viewModel.patchdone) {
SelectionContainer {
Text(
modifier = Modifier.padding(8.dp),
text = viewModel.patchLog,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
)
}
LaunchedEffect(viewModel.patchLog) {
scrollState.animateScrollTo(scrollState.maxValue)
}
}
Spacer(modifier = Modifier.height(12.dp))
// loading progress
if (viewModel.running) {
Box(
modifier = Modifier
.padding(innerPadding)
.align(Alignment.CenterHorizontally)
) {
CircularProgressIndicator(
modifier = Modifier
.size(50.dp)
.padding(16.dp)
.align(Alignment.BottomCenter)
)
}
}
}
}
}
@Composable
private fun StartButton(text: String, onClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
onClick = onClick,
content = {
Text(text = text)
}
)
}
}
@Composable
private fun ExtraItem(extra: KPModel.IExtraInfo, existed: Boolean, onDelete: () -> Unit) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
}),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
) {
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text(
text = stringResource(
id =
if (existed) R.string.patch_item_existed_extra_kpm else R.string.patch_item_new_extra_kpm
) +
" " + extra.type.toString().uppercase(),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.CenterHorizontally)
)
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
modifier = Modifier
.padding(end = 8.dp)
.clickable { onDelete() })
}
if (extra.type == KPModel.ExtraType.KPM) {
val kpmInfo: KPModel.KPMInfo = extra as KPModel.KPMInfo
Text(
text = "${stringResource(id = R.string.patch_item_extra_name) + " "} ${kpmInfo.name}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(id = R.string.patch_item_extra_version) + " "} ${kpmInfo.version}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(id = R.string.patch_item_extra_kpm_license) + " "} ${kpmInfo.license}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(id = R.string.patch_item_extra_author) + " "} ${kpmInfo.author}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(id = R.string.patch_item_extra_kpm_desciption) + " "} ${kpmInfo.description}",
style = MaterialTheme.typography.bodyMedium
)
var event by remember { mutableStateOf(kpmInfo.event) }
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
) {
Text(
text = stringResource(id = R.string.patch_item_extra_event) + " ",
style = MaterialTheme.typography.bodyMedium
)
BasicTextField(
modifier = Modifier.fillMaxWidth(),
value = event,
textStyle = MaterialTheme.typography.bodyMedium,
onValueChange = {
event = it
kpmInfo.event = it
},
)
}
var args by remember { mutableStateOf(kpmInfo.args) }
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
) {
Text(
text = stringResource(id = R.string.patch_item_extra_args) + " ",
style = MaterialTheme.typography.bodyMedium
)
BasicTextField(
modifier = Modifier.fillMaxWidth(),
value = args,
textStyle = MaterialTheme.typography.bodyMedium,
onValueChange = {
args = it
kpmInfo.args = it
},
)
}
}
}
}
}
@Composable
private fun SetSuperKeyView(viewModel: PatchesViewModel) {
var skey by remember { mutableStateOf(viewModel.superkey) }
var showWarn by remember { mutableStateOf(!viewModel.checkSuperKeyValidation(skey)) }
var keyVisible by remember { mutableStateOf(false) }
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.patch_item_skey),
style = MaterialTheme.typography.bodyLarge
)
}
if (showWarn) {
Spacer(modifier = Modifier.height(3.dp))
Text(
color = Color.Red,
text = stringResource(id = R.string.patch_item_set_skey_label),
style = MaterialTheme.typography.bodyMedium
)
}
Column {
//Spacer(modifier = Modifier.height(8.dp))
Box(
contentAlignment = Alignment.CenterEnd,
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 6.dp),
value = skey,
label = { Text(stringResource(id = R.string.patch_set_superkey)) },
visualTransformation = if (keyVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
shape = RoundedCornerShape(50.0f),
onValueChange = {
skey = it
if (viewModel.checkSuperKeyValidation(it)) {
viewModel.superkey = it
showWarn = false
} else {
viewModel.superkey = ""
showWarn = true
}
},
)
IconButton(
modifier = Modifier
.size(40.dp)
.padding(top = 15.dp, end = 5.dp),
onClick = { keyVisible = !keyVisible }
) {
Icon(
imageVector = if (keyVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
contentDescription = null,
tint = Color.Gray
)
}
}
}
}
}
}
@Composable
private fun KernelPatchImageView(kpImgInfo: KPModel.KPImgInfo) {
if (kpImgInfo.version.isEmpty()) return
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.patch_item_kpimg),
style = MaterialTheme.typography.bodyLarge
)
}
Text(
text = stringResource(id = R.string.patch_item_kpimg_version) + " " + Version.uInt2String(
kpImgInfo.version.substring(2).toUInt(16)
), style = MaterialTheme.typography.bodyMedium
)
Text(
text = stringResource(id = R.string.patch_item_kpimg_comile_time) + " " + kpImgInfo.compileTime,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = stringResource(id = R.string.patch_item_kpimg_config) + " " + kpImgInfo.config,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
private fun BootimgView(slot: String, boot: String) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.patch_item_bootimg),
style = MaterialTheme.typography.bodyLarge
)
}
if (slot.isNotEmpty()) {
Text(
text = stringResource(id = R.string.patch_item_bootimg_slot) + " " + slot,
style = MaterialTheme.typography.bodyMedium
)
}
Text(
text = stringResource(id = R.string.patch_item_bootimg_dev) + " " + boot,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
private fun KernelImageView(kImgInfo: KPModel.KImgInfo) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.patch_item_kernel),
style = MaterialTheme.typography.bodyLarge
)
}
Text(text = kImgInfo.banner, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
private fun SelectFileButton(text: String, onSelected: (data: Intent, uri: Uri) -> Unit) {
val selectFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode != Activity.RESULT_OK) {
return@rememberLauncherForActivityResult
}
val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult
onSelected(data, uri)
}
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Button(
onClick = {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
selectFileLauncher.launch(intent)
},
content = { Text(text = text) }
)
}
}
@Composable
private fun ErrorView(error: String) {
if (error.isEmpty()) return
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.error
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.patch_item_error),
style = MaterialTheme.typography.bodyLarge
)
Text(text = error, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
private fun PatchMode(mode: PatchesViewModel.PatchMode) {
ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run {
MaterialTheme.colorScheme.secondaryContainer
})
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = stringResource(id = mode.sId), style = MaterialTheme.typography.bodyLarge)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar() {
TopAppBar(title = { Text(stringResource(R.string.patch_config_title)) })
}

View file

@ -0,0 +1,751 @@
package me.bmax.apatch.ui.screen
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.Commit
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.DeveloperMode
import androidx.compose.material.icons.filled.Engineering
import androidx.compose.material.icons.filled.FilePresent
import androidx.compose.material.icons.filled.FormatColorFill
import androidx.compose.material.icons.filled.InvertColors
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.RemoveFromQueue
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.bmax.apatch.APApplication
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.Natives
import me.bmax.apatch.R
import me.bmax.apatch.ui.component.SwitchItem
import me.bmax.apatch.ui.component.rememberConfirmDialog
import me.bmax.apatch.ui.component.rememberLoadingDialog
import me.bmax.apatch.ui.theme.refreshTheme
import me.bmax.apatch.util.APatchKeyHelper
import me.bmax.apatch.util.getBugreportFile
import me.bmax.apatch.util.isForceUsingOverlayFS
import me.bmax.apatch.util.isGlobalNamespaceEnabled
import me.bmax.apatch.util.isLiteModeEnabled
import me.bmax.apatch.util.outputStream
import me.bmax.apatch.util.overlayFsAvailable
import me.bmax.apatch.util.rootShellForResult
import me.bmax.apatch.util.setForceUsingOverlayFS
import me.bmax.apatch.util.setGlobalNamespaceEnabled
import me.bmax.apatch.util.setLiteMode
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
import me.bmax.apatch.util.ui.LocalSnackbarHost
import me.bmax.apatch.util.ui.NavigationBarsSpacer
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
@Destination<RootGraph>
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun SettingScreen() {
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
val kPatchReady = state != APApplication.State.UNKNOWN_STATE
val aPatchReady =
(state == APApplication.State.ANDROIDPATCH_INSTALLING || state == APApplication.State.ANDROIDPATCH_INSTALLED || state == APApplication.State.ANDROIDPATCH_NEED_UPDATE)
var isGlobalNamespaceEnabled by rememberSaveable {
mutableStateOf(false)
}
var isLiteModeEnabled by rememberSaveable {
mutableStateOf(false)
}
var forceUsingOverlayFS by rememberSaveable {
mutableStateOf(false)
}
var bSkipStoreSuperKey by rememberSaveable {
mutableStateOf(APatchKeyHelper.shouldSkipStoreSuperKey())
}
val isOverlayFSAvailable by rememberSaveable {
mutableStateOf(overlayFsAvailable())
}
if (kPatchReady && aPatchReady) {
isGlobalNamespaceEnabled = isGlobalNamespaceEnabled()
isLiteModeEnabled = isLiteModeEnabled()
forceUsingOverlayFS = isForceUsingOverlayFS()
}
val snackBarHost = LocalSnackbarHost.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.settings)) },
)
},
snackbarHost = { SnackbarHost(snackBarHost) }
) { paddingValues ->
val loadingDialog = rememberLoadingDialog()
val clearKeyDialog = rememberConfirmDialog(
onConfirm = {
APatchKeyHelper.clearConfigKey()
APApplication.superKey = ""
}
)
val showLanguageDialog = rememberSaveable { mutableStateOf(false) }
LanguageDialog(showLanguageDialog)
val showResetSuPathDialog = remember { mutableStateOf(false) }
if (showResetSuPathDialog.value) {
ResetSUPathDialog(showResetSuPathDialog)
}
val showThemeChooseDialog = remember { mutableStateOf(false) }
if (showThemeChooseDialog.value) {
ThemeChooseDialog(showThemeChooseDialog)
}
var showLogBottomSheet by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val context = LocalContext.current
val logSavedMessage = stringResource(R.string.log_saved)
val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? ->
if (uri != null) {
scope.launch(Dispatchers.IO) {
loadingDialog.show()
uri.outputStream().use { output ->
getBugreportFile(context).inputStream().use {
it.copyTo(output)
}
}
loadingDialog.hide()
snackBarHost.showSnackbar(message = logSavedMessage)
}
}
}
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val prefs = APApplication.sharedPreferences
// clear key
if (kPatchReady) {
val clearKeyDialogTitle = stringResource(id = R.string.clear_super_key)
val clearKeyDialogContent =
stringResource(id = R.string.settings_clear_super_key_dialog)
ListItem(
leadingContent = {
Icon(
Icons.Filled.Key, stringResource(id = R.string.super_key)
)
},
headlineContent = { Text(stringResource(id = R.string.clear_super_key)) },
modifier = Modifier.clickable {
clearKeyDialog.showConfirm(
title = clearKeyDialogTitle,
content = clearKeyDialogContent,
markdown = false,
)
})
}
// store key local?
SwitchItem(
icon = Icons.Filled.Key,
title = stringResource(id = R.string.settings_donot_store_superkey),
summary = stringResource(id = R.string.settings_donot_store_superkey_summary),
checked = bSkipStoreSuperKey,
onCheckedChange = {
bSkipStoreSuperKey = it
APatchKeyHelper.setShouldSkipStoreSuperKey(bSkipStoreSuperKey)
})
// Global mount
if (kPatchReady && aPatchReady) {
SwitchItem(
icon = Icons.Filled.Engineering,
title = stringResource(id = R.string.settings_global_namespace_mode),
summary = stringResource(id = R.string.settings_global_namespace_mode_summary),
checked = isGlobalNamespaceEnabled,
onCheckedChange = {
setGlobalNamespaceEnabled(
if (isGlobalNamespaceEnabled) {
"0"
} else {
"1"
}
)
isGlobalNamespaceEnabled = it
})
}
// Lite Mode
if (kPatchReady && aPatchReady) {
SwitchItem(
icon = Icons.Filled.RemoveFromQueue,
title = stringResource(id = R.string.settings_lite_mode),
summary = stringResource(id = R.string.settings_lite_mode_mode_summary),
checked = isLiteModeEnabled,
onCheckedChange = {
setLiteMode(it)
isLiteModeEnabled = it
})
}
// Force OverlayFS
if (kPatchReady && aPatchReady && isOverlayFSAvailable) {
SwitchItem(
icon = Icons.Filled.FilePresent,
title = stringResource(id = R.string.settings_force_overlayfs_mode),
summary = stringResource(id = R.string.settings_force_overlayfs_mode_summary),
checked = forceUsingOverlayFS,
onCheckedChange = {
setForceUsingOverlayFS(it)
forceUsingOverlayFS = it
})
}
// WebView Debug
if (aPatchReady) {
var enableWebDebugging by rememberSaveable {
mutableStateOf(
prefs.getBoolean("enable_web_debugging", false)
)
}
SwitchItem(
icon = Icons.Filled.DeveloperMode,
title = stringResource(id = R.string.enable_web_debugging),
summary = stringResource(id = R.string.enable_web_debugging_summary),
checked = enableWebDebugging
) {
APApplication.sharedPreferences.edit {
putBoolean("enable_web_debugging", it)
}
enableWebDebugging = it
}
}
// Check Update
var checkUpdate by rememberSaveable {
mutableStateOf(
prefs.getBoolean("check_update", true)
)
}
SwitchItem(
icon = Icons.Filled.Update,
title = stringResource(id = R.string.settings_check_update),
summary = stringResource(id = R.string.settings_check_update_summary),
checked = checkUpdate
) {
prefs.edit { putBoolean("check_update", it) }
checkUpdate = it
}
// Night Mode Follow System
var nightFollowSystem by rememberSaveable {
mutableStateOf(
prefs.getBoolean("night_mode_follow_sys", true)
)
}
SwitchItem(
icon = Icons.Filled.InvertColors,
title = stringResource(id = R.string.settings_night_mode_follow_sys),
summary = stringResource(id = R.string.settings_night_mode_follow_sys_summary),
checked = nightFollowSystem
) {
prefs.edit { putBoolean("night_mode_follow_sys", it) }
nightFollowSystem = it
refreshTheme.value = true
}
// Custom Night Theme Switch
if (!nightFollowSystem) {
var nightThemeEnabled by rememberSaveable {
mutableStateOf(
prefs.getBoolean("night_mode_enabled", false)
)
}
SwitchItem(
icon = Icons.Filled.DarkMode,
title = stringResource(id = R.string.settings_night_theme_enabled),
checked = nightThemeEnabled
) {
prefs.edit { putBoolean("night_mode_enabled", it) }
nightThemeEnabled = it
refreshTheme.value = true
}
}
// System dynamic color theme
val isDynamicColorSupport = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
if (isDynamicColorSupport) {
var useSystemDynamicColor by rememberSaveable {
mutableStateOf(
prefs.getBoolean("use_system_color_theme", true)
)
}
SwitchItem(
icon = Icons.Filled.ColorLens,
title = stringResource(id = R.string.settings_use_system_color_theme),
summary = stringResource(id = R.string.settings_use_system_color_theme_summary),
checked = useSystemDynamicColor
) {
prefs.edit { putBoolean("use_system_color_theme", it) }
useSystemDynamicColor = it
refreshTheme.value = true
}
if (!useSystemDynamicColor) {
ListItem(headlineContent = {
Text(text = stringResource(id = R.string.settings_custom_color_theme))
}, modifier = Modifier.clickable {
showThemeChooseDialog.value = true
}, supportingContent = {
val colorMode = prefs.getString("custom_color", "blue")
Text(
text = stringResource(colorNameToString(colorMode.toString())),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}, leadingContent = { Icon(Icons.Filled.FormatColorFill, null) })
}
} else {
ListItem(headlineContent = {
Text(text = stringResource(id = R.string.settings_custom_color_theme))
}, modifier = Modifier.clickable {
showThemeChooseDialog.value = true
}, supportingContent = {
val colorMode = prefs.getString("custom_color", "blue")
Text(
text = stringResource(colorNameToString(colorMode.toString())),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}, leadingContent = { Icon(Icons.Filled.FormatColorFill, null) })
}
// su path
if (kPatchReady) {
ListItem(
leadingContent = {
Icon(
Icons.Filled.Commit, stringResource(id = R.string.setting_reset_su_path)
)
},
supportingContent = {},
headlineContent = { Text(stringResource(id = R.string.setting_reset_su_path)) },
modifier = Modifier.clickable {
showResetSuPathDialog.value = true
})
}
// language
ListItem(headlineContent = {
Text(text = stringResource(id = R.string.settings_app_language))
}, modifier = Modifier.clickable {
showLanguageDialog.value = true
}, supportingContent = {
Text(text = AppCompatDelegate.getApplicationLocales()[0]?.displayLanguage?.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
} ?: stringResource(id = R.string.system_default),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline)
}, leadingContent = { Icon(Icons.Filled.Translate, null) })
// log
ListItem(
leadingContent = {
Icon(
Icons.Filled.BugReport, stringResource(id = R.string.send_log)
)
},
headlineContent = { Text(stringResource(id = R.string.send_log)) },
modifier = Modifier.clickable {
showLogBottomSheet = true
})
if (showLogBottomSheet) {
ModalBottomSheet(
onDismissRequest = { showLogBottomSheet = false },
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
content = {
Row(
modifier = Modifier
.padding(10.dp)
.align(Alignment.CenterHorizontally)
) {
Box {
Column(
modifier = Modifier
.padding(16.dp)
.clickable {
scope.launch {
val formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("APatch_bugreport_${current}.tar.gz")
showLogBottomSheet = false
}
}
) {
Icon(
Icons.Filled.Save,
contentDescription = null,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
text = stringResource(id = R.string.save_log),
modifier = Modifier.padding(top = 16.dp),
textAlign = TextAlign.Center.also {
LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None
)
}
)
}
}
Box {
Column(
modifier = Modifier
.padding(16.dp)
.clickable {
scope.launch {
val bugreport = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
getBugreportFile(context)
}
}
val uri: Uri = FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
bugreport
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
setDataAndType(uri, "application/gzip")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(
shareIntent,
context.getString(R.string.send_log)
)
)
showLogBottomSheet = false
}
}) {
Icon(
Icons.Filled.Share,
contentDescription = null,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
text = stringResource(id = R.string.send_log),
modifier = Modifier.padding(top = 16.dp),
textAlign = TextAlign.Center.also {
LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None
)
}
)
}
}
}
NavigationBarsSpacer()
})
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThemeChooseDialog(showDialog: MutableState<Boolean>) {
val prefs = APApplication.sharedPreferences
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
)
) {
Surface(
modifier = Modifier
.width(310.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(30.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
LazyColumn {
items(colorsList()) {
ListItem(
headlineContent = { Text(text = stringResource(it.nameId)) },
modifier = Modifier.clickable {
showDialog.value = false
prefs.edit { putString("custom_color", it.name) }
refreshTheme.value = true
})
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
}
private data class APColor(
val name: String, @param:StringRes val nameId: Int
)
private fun colorsList(): List<APColor> {
return listOf(
APColor("amber", R.string.amber_theme),
APColor("blue_grey", R.string.blue_grey_theme),
APColor("blue", R.string.blue_theme),
APColor("brown", R.string.brown_theme),
APColor("cyan", R.string.cyan_theme),
APColor("deep_orange", R.string.deep_orange_theme),
APColor("deep_purple", R.string.deep_purple_theme),
APColor("green", R.string.green_theme),
APColor("indigo", R.string.indigo_theme),
APColor("light_blue", R.string.light_blue_theme),
APColor("light_green", R.string.light_green_theme),
APColor("lime", R.string.lime_theme),
APColor("orange", R.string.orange_theme),
APColor("pink", R.string.pink_theme),
APColor("purple", R.string.purple_theme),
APColor("red", R.string.red_theme),
APColor("sakura", R.string.sakura_theme),
APColor("teal", R.string.teal_theme),
APColor("yellow", R.string.yellow_theme),
)
}
@Composable
private fun colorNameToString(colorName: String): Int {
return colorsList().find { it.name == colorName }?.nameId ?: R.string.blue_theme
}
val suPathChecked: (path: String) -> Boolean = {
it.startsWith("/") && it.trim().length > 1
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ResetSUPathDialog(showDialog: MutableState<Boolean>) {
val context = LocalContext.current
var suPath by remember { mutableStateOf(Natives.suPath()) }
BasicAlertDialog(
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
decorFitsSystemWindows = true,
usePlatformDefaultWidth = false,
)
) {
Surface(
modifier = Modifier
.width(310.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(30.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
Box(
Modifier
.padding(PaddingValues(bottom = 16.dp))
.align(Alignment.Start)
) {
Text(
text = stringResource(id = R.string.setting_reset_su_path),
style = MaterialTheme.typography.headlineSmall
)
}
Box(
Modifier
.weight(weight = 1f, fill = false)
.padding(PaddingValues(bottom = 12.dp))
.align(Alignment.Start)
) {
OutlinedTextField(
value = suPath,
onValueChange = {
suPath = it
},
label = { Text(stringResource(id = R.string.setting_reset_su_new_path)) },
visualTransformation = VisualTransformation.None,
)
}
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDialog.value = false }) {
Text(stringResource(id = android.R.string.cancel))
}
Button(enabled = suPathChecked(suPath), onClick = {
showDialog.value = false
val success = Natives.resetSuPath(suPath)
Toast.makeText(
context,
if (success) R.string.success else R.string.failure,
Toast.LENGTH_SHORT
).show()
rootShellForResult("echo $suPath > ${APApplication.SU_PATH_FILE}")
}) {
Text(stringResource(id = android.R.string.ok))
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LanguageDialog(showLanguageDialog: MutableState<Boolean>) {
val languages = stringArrayResource(id = R.array.languages)
val languagesValues = stringArrayResource(id = R.array.languages_values)
if (showLanguageDialog.value) {
BasicAlertDialog(
onDismissRequest = { showLanguageDialog.value = false }
) {
Surface(
modifier = Modifier
.width(150.dp)
.wrapContentHeight(),
shape = RoundedCornerShape(28.dp),
tonalElevation = AlertDialogDefaults.TonalElevation,
color = AlertDialogDefaults.containerColor,
) {
LazyColumn {
itemsIndexed(languages) { index, item ->
ListItem(
headlineContent = { Text(item) },
modifier = Modifier.clickable {
showLanguageDialog.value = false
if (index == 0) {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.getEmptyLocaleList()
)
} else {
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
languagesValues[index]
)
)
}
}
)
}
}
}
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
}
}
}

View file

@ -0,0 +1,267 @@
package me.bmax.apatch.ui.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.filled.MoreVert
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.launch
import me.bmax.apatch.APApplication
import me.bmax.apatch.Natives
import me.bmax.apatch.R
import me.bmax.apatch.ui.component.ProvideMenuShape
import me.bmax.apatch.ui.component.SearchAppBar
import me.bmax.apatch.ui.component.SwitchItem
import me.bmax.apatch.ui.viewmodel.SuperUserViewModel
import me.bmax.apatch.util.PkgConfig
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SuperUserScreen() {
val viewModel = viewModel<SuperUserViewModel>()
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
if (viewModel.appList.isEmpty()) {
viewModel.fetchAppList()
}
}
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.su_title)) },
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
dropdownContent = {
var showDropdown by remember { mutableStateOf(false) }
IconButton(
onClick = { showDropdown = true },
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings)
)
ProvideMenuShape(RoundedCornerShape(10.dp)) {
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
DropdownMenuItem(text = {
Text(stringResource(R.string.su_refresh))
}, onClick = {
scope.launch {
viewModel.fetchAppList()
}
showDropdown = false
})
DropdownMenuItem(text = {
Text(
if (viewModel.showSystemApps) {
stringResource(R.string.su_hide_system_apps)
} else {
stringResource(R.string.su_show_system_apps)
}
)
}, onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false
})
}
}
}
},
)
},
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier.padding(innerPadding),
onRefresh = { scope.launch { viewModel.fetchAppList() } },
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(Modifier.fillMaxSize()) {
items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
AppItem(app)
}
}
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun AppItem(
app: SuperUserViewModel.AppInfo,
) {
val config = app.config
var showEditProfile by remember { mutableStateOf(false) }
var rootGranted by remember { mutableStateOf(config.allow != 0) }
var excludeApp by remember { mutableIntStateOf(config.exclude) }
ListItem(
modifier = Modifier.clickable(onClick = {
if (!rootGranted) {
showEditProfile = !showEditProfile
} else {
rootGranted = false
config.allow = 0
Natives.revokeSu(app.uid)
PkgConfig.changeConfig(config)
}
}),
headlineContent = { Text(app.label) },
leadingContent = {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(app.packageInfo)
.crossfade(true).build(),
contentDescription = app.label,
modifier = Modifier
.padding(4.dp)
.width(48.dp)
.height(48.dp)
)
},
supportingContent = {
Column {
Text(app.packageName)
FlowRow {
if (excludeApp == 1) {
LabelText(label = stringResource(id = R.string.su_pkg_excluded_label))
}
if (rootGranted) {
LabelText(label = config.profile.uid.toString())
LabelText(label = config.profile.toUid.toString())
LabelText(
label = when {
// todo: valid scontext ?
config.profile.scontext.isNotEmpty() -> config.profile.scontext
else -> stringResource(id = R.string.su_selinux_via_hook)
}
)
}
}
}
},
trailingContent = {
Switch(checked = rootGranted, onCheckedChange = {
rootGranted = !rootGranted
if (rootGranted) {
excludeApp = 0
config.allow = 1
config.exclude = 0
config.profile.scontext = APApplication.MAGISK_SCONTEXT
} else {
config.allow = 0
}
config.profile.uid = app.uid
PkgConfig.changeConfig(config)
if (config.allow == 1) {
Natives.grantSu(app.uid, 0, config.profile.scontext)
Natives.setUidExclude(app.uid, 0)
} else {
Natives.revokeSu(app.uid)
}
})
},
)
AnimatedVisibility(
visible = showEditProfile && !rootGranted,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) {
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.su_pkg_excluded_setting_title),
summary = stringResource(id = R.string.su_pkg_excluded_setting_summary),
checked = excludeApp == 1,
onCheckedChange = {
if (it) {
excludeApp = 1
config.allow = 0
config.profile.scontext = APApplication.DEFAULT_SCONTEXT
Natives.revokeSu(app.uid)
} else {
excludeApp = 0
}
config.exclude = excludeApp
config.profile.uid = app.uid
PkgConfig.changeConfig(config)
Natives.setUidExclude(app.uid, excludeApp)
},
)
}
}
@Composable
fun LabelText(label: String) {
Box(
modifier = Modifier
.padding(top = 4.dp, end = 4.dp)
.background(
Color.Black, shape = RoundedCornerShape(4.dp)
)
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
style = TextStyle(
fontSize = 8.sp,
color = Color.White,
)
)
}
}

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF785900)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFDF9E)
private val md_theme_light_onPrimaryContainer = Color(0xFF261A00)
private val md_theme_light_secondary = Color(0xFF6B5D3F)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFF5E0BB)
private val md_theme_light_onSecondaryContainer = Color(0xFF241A04)
private val md_theme_light_tertiary = Color(0xFF4A6547)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFCCEBC4)
private val md_theme_light_onTertiaryContainer = Color(0xFF072109)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF1E1B16)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF1E1B16)
private val md_theme_light_surfaceVariant = Color(0xFFEDE1CF)
private val md_theme_light_onSurfaceVariant = Color(0xFF4D4639)
private val md_theme_light_outline = Color(0xFF7F7667)
private val md_theme_light_inverseOnSurface = Color(0xFFF7EFE7)
private val md_theme_light_inverseSurface = Color(0xFF33302A)
private val md_theme_light_inversePrimary = Color(0xFFFABD00)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF785900)
private val md_theme_light_outlineVariant = Color(0xFFD0C5B4)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFABD00)
private val md_theme_dark_onPrimary = Color(0xFF3F2E00)
private val md_theme_dark_primaryContainer = Color(0xFF5B4300)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDF9E)
private val md_theme_dark_secondary = Color(0xFFD8C4A0)
private val md_theme_dark_onSecondary = Color(0xFF3A2F15)
private val md_theme_dark_secondaryContainer = Color(0xFF52452A)
private val md_theme_dark_onSecondaryContainer = Color(0xFFF5E0BB)
private val md_theme_dark_tertiary = Color(0xFFB0CFAA)
private val md_theme_dark_onTertiary = Color(0xFF1D361C)
private val md_theme_dark_tertiaryContainer = Color(0xFF334D31)
private val md_theme_dark_onTertiaryContainer = Color(0xFFCCEBC4)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1E1B16)
private val md_theme_dark_onBackground = Color(0xFFE9E1D8)
private val md_theme_dark_surface = Color(0xFF1E1B16)
private val md_theme_dark_onSurface = Color(0xFFE9E1D8)
private val md_theme_dark_surfaceVariant = Color(0xFF4D4639)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD0C5B4)
private val md_theme_dark_outline = Color(0xFF998F80)
private val md_theme_dark_inverseOnSurface = Color(0xFF1E1B16)
private val md_theme_dark_inverseSurface = Color(0xFFE9E1D8)
private val md_theme_dark_inversePrimary = Color(0xFF785900)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFABD00)
private val md_theme_dark_outlineVariant = Color(0xFF4D4639)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightAmberTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkAmberTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF00668A)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFC4E7FF)
private val md_theme_light_onPrimaryContainer = Color(0xFF001E2C)
private val md_theme_light_secondary = Color(0xFF4E616D)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFD1E5F4)
private val md_theme_light_onSecondaryContainer = Color(0xFF0A1E28)
private val md_theme_light_tertiary = Color(0xFF605A7D)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFE6DEFF)
private val md_theme_light_onTertiaryContainer = Color(0xFF1D1736)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFBFCFF)
private val md_theme_light_onBackground = Color(0xFF191C1E)
private val md_theme_light_surface = Color(0xFFFBFCFF)
private val md_theme_light_onSurface = Color(0xFF191C1E)
private val md_theme_light_surfaceVariant = Color(0xFFDCE3E9)
private val md_theme_light_onSurfaceVariant = Color(0xFF41484D)
private val md_theme_light_outline = Color(0xFF71787D)
private val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
private val md_theme_light_inverseSurface = Color(0xFF2E3133)
private val md_theme_light_inversePrimary = Color(0xFF7BD0FF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF00668A)
private val md_theme_light_outlineVariant = Color(0xFFC0C7CD)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF7BD0FF)
private val md_theme_dark_onPrimary = Color(0xFF003549)
private val md_theme_dark_primaryContainer = Color(0xFF004C69)
private val md_theme_dark_onPrimaryContainer = Color(0xFFC4E7FF)
private val md_theme_dark_secondary = Color(0xFFB5C9D7)
private val md_theme_dark_onSecondary = Color(0xFF20333E)
private val md_theme_dark_secondaryContainer = Color(0xFF374955)
private val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F4)
private val md_theme_dark_tertiary = Color(0xFFCAC1E9)
private val md_theme_dark_onTertiary = Color(0xFF322C4C)
private val md_theme_dark_tertiaryContainer = Color(0xFF484264)
private val md_theme_dark_onTertiaryContainer = Color(0xFFE6DEFF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF191C1E)
private val md_theme_dark_onBackground = Color(0xFFE1E2E5)
private val md_theme_dark_surface = Color(0xFF191C1E)
private val md_theme_dark_onSurface = Color(0xFFE1E2E5)
private val md_theme_dark_surfaceVariant = Color(0xFF41484D)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD)
private val md_theme_dark_outline = Color(0xFF8B9297)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
private val md_theme_dark_inverseSurface = Color(0xFFE1E2E5)
private val md_theme_dark_inversePrimary = Color(0xFF00668A)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF7BD0FF)
private val md_theme_dark_outlineVariant = Color(0xFF41484D)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightBlueGreyTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkBlueGreyTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,132 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF0061A4)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFD1E4FF)
private val md_theme_light_onPrimaryContainer = Color(0xFF001D36)
private val md_theme_light_secondary = Color(0xFF535F70)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFD7E3F7)
private val md_theme_light_onSecondaryContainer = Color(0xFF101C2B)
private val md_theme_light_tertiary = Color(0xFF6B5778)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFF2DAFF)
private val md_theme_light_onTertiaryContainer = Color(0xFF251431)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFDFCFF)
private val md_theme_light_onBackground = Color(0xFF1A1C1E)
private val md_theme_light_surface = Color(0xFFFDFCFF)
private val md_theme_light_onSurface = Color(0xFF1A1C1E)
private val md_theme_light_surfaceVariant = Color(0xFFDFE2EB)
private val md_theme_light_onSurfaceVariant = Color(0xFF43474E)
private val md_theme_light_outline = Color(0xFF73777F)
private val md_theme_light_inverseOnSurface = Color(0xFFF1F0F4)
private val md_theme_light_inverseSurface = Color(0xFF2F3033)
private val md_theme_light_inversePrimary = Color(0xFF9ECAFF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF0061A4)
private val md_theme_light_outlineVariant = Color(0xFFC3C7CF)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF9ECAFF)
private val md_theme_dark_onPrimary = Color(0xFF003258)
private val md_theme_dark_primaryContainer = Color(0xFF00497D)
private val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF)
private val md_theme_dark_secondary = Color(0xFFBBC7DB)
private val md_theme_dark_onSecondary = Color(0xFF253140)
private val md_theme_dark_secondaryContainer = Color(0xFF3B4858)
private val md_theme_dark_onSecondaryContainer = Color(0xFFD7E3F7)
private val md_theme_dark_tertiary = Color(0xFFD6BEE4)
private val md_theme_dark_onTertiary = Color(0xFF3B2948)
private val md_theme_dark_tertiaryContainer = Color(0xFF523F5F)
private val md_theme_dark_onTertiaryContainer = Color(0xFFF2DAFF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1A1C1E)
private val md_theme_dark_onBackground = Color(0xFFE2E2E6)
private val md_theme_dark_surface = Color(0xFF1A1C1E)
private val md_theme_dark_onSurface = Color(0xFFE2E2E6)
private val md_theme_dark_surfaceVariant = Color(0xFF43474E)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC3C7CF)
private val md_theme_dark_outline = Color(0xFF8D9199)
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E)
private val md_theme_dark_inverseSurface = Color(0xFFE2E2E6)
private val md_theme_dark_inversePrimary = Color(0xFF0061A4)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF9ECAFF)
private val md_theme_dark_outlineVariant = Color(0xFF43474E)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightBlueTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkBlueTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF9A4522)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFDBCF)
private val md_theme_light_onPrimaryContainer = Color(0xFF380D00)
private val md_theme_light_secondary = Color(0xFF77574C)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFDBCF)
private val md_theme_light_onSecondaryContainer = Color(0xFF2C160D)
private val md_theme_light_tertiary = Color(0xFF695E2F)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFF2E2A8)
private val md_theme_light_onTertiaryContainer = Color(0xFF211B00)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201A18)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201A18)
private val md_theme_light_surfaceVariant = Color(0xFFF5DED6)
private val md_theme_light_onSurfaceVariant = Color(0xFF53433E)
private val md_theme_light_outline = Color(0xFF85736D)
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEA)
private val md_theme_light_inverseSurface = Color(0xFF362F2C)
private val md_theme_light_inversePrimary = Color(0xFFFFB59A)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF9A4522)
private val md_theme_light_outlineVariant = Color(0xFFD8C2BB)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB59A)
private val md_theme_dark_onPrimary = Color(0xFF5B1B00)
private val md_theme_dark_primaryContainer = Color(0xFF7B2E0D)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBCF)
private val md_theme_dark_secondary = Color(0xFFE7BEAF)
private val md_theme_dark_onSecondary = Color(0xFF442A20)
private val md_theme_dark_secondaryContainer = Color(0xFF5D4035)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBCF)
private val md_theme_dark_tertiary = Color(0xFFD5C68E)
private val md_theme_dark_onTertiary = Color(0xFF393005)
private val md_theme_dark_tertiaryContainer = Color(0xFF50471A)
private val md_theme_dark_onTertiaryContainer = Color(0xFFF2E2A8)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201A18)
private val md_theme_dark_onBackground = Color(0xFFEDE0DC)
private val md_theme_dark_surface = Color(0xFF201A18)
private val md_theme_dark_onSurface = Color(0xFFEDE0DC)
private val md_theme_dark_surfaceVariant = Color(0xFF53433E)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BB)
private val md_theme_dark_outline = Color(0xFFA08D86)
private val md_theme_dark_inverseOnSurface = Color(0xFF201A18)
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DC)
private val md_theme_dark_inversePrimary = Color(0xFF9A4522)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB59A)
private val md_theme_dark_outlineVariant = Color(0xFF53433E)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightBrownTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkBrownTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF006876)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFA1EFFF)
private val md_theme_light_onPrimaryContainer = Color(0xFF001F25)
private val md_theme_light_secondary = Color(0xFF4A6268)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFCDE7ED)
private val md_theme_light_onSecondaryContainer = Color(0xFF051F23)
private val md_theme_light_tertiary = Color(0xFF545D7E)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFDBE1FF)
private val md_theme_light_onTertiaryContainer = Color(0xFF101A37)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFBFCFD)
private val md_theme_light_onBackground = Color(0xFF191C1D)
private val md_theme_light_surface = Color(0xFFFBFCFD)
private val md_theme_light_onSurface = Color(0xFF191C1D)
private val md_theme_light_surfaceVariant = Color(0xFFDBE4E6)
private val md_theme_light_onSurfaceVariant = Color(0xFF3F484A)
private val md_theme_light_outline = Color(0xFF6F797B)
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2)
private val md_theme_light_inverseSurface = Color(0xFF2E3132)
private val md_theme_light_inversePrimary = Color(0xFF44D8F1)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF006876)
private val md_theme_light_outlineVariant = Color(0xFFBFC8CA)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF44D8F1)
private val md_theme_dark_onPrimary = Color(0xFF00363E)
private val md_theme_dark_primaryContainer = Color(0xFF004E59)
private val md_theme_dark_onPrimaryContainer = Color(0xFFA1EFFF)
private val md_theme_dark_secondary = Color(0xFFB1CBD1)
private val md_theme_dark_onSecondary = Color(0xFF1C3439)
private val md_theme_dark_secondaryContainer = Color(0xFF334A50)
private val md_theme_dark_onSecondaryContainer = Color(0xFFCDE7ED)
private val md_theme_dark_tertiary = Color(0xFFBCC5EB)
private val md_theme_dark_onTertiary = Color(0xFF262F4D)
private val md_theme_dark_tertiaryContainer = Color(0xFF3C4665)
private val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF191C1D)
private val md_theme_dark_onBackground = Color(0xFFE1E3E3)
private val md_theme_dark_surface = Color(0xFF191C1D)
private val md_theme_dark_onSurface = Color(0xFFE1E3E3)
private val md_theme_dark_surfaceVariant = Color(0xFF3F484A)
private val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CA)
private val md_theme_dark_outline = Color(0xFF899295)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1D)
private val md_theme_dark_inverseSurface = Color(0xFFE1E3E3)
private val md_theme_dark_inversePrimary = Color(0xFF006876)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF44D8F1)
private val md_theme_dark_outlineVariant = Color(0xFF3F484A)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightCyanTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkCyanTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,132 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFFB02F00)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFDBD1)
private val md_theme_light_onPrimaryContainer = Color(0xFF3B0900)
private val md_theme_light_secondary = Color(0xFF77574E)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFDBD1)
private val md_theme_light_onSecondaryContainer = Color(0xFF2C150F)
private val md_theme_light_tertiary = Color(0xFF6C5D2F)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFF5E1A7)
private val md_theme_light_onTertiaryContainer = Color(0xFF231B00)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201A18)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201A18)
private val md_theme_light_surfaceVariant = Color(0xFFF5DED8)
private val md_theme_light_onSurfaceVariant = Color(0xFF53433F)
private val md_theme_light_outline = Color(0xFF85736E)
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEB)
private val md_theme_light_inverseSurface = Color(0xFF362F2D)
private val md_theme_light_inversePrimary = Color(0xFFFFB5A0)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFFB02F00)
private val md_theme_light_outlineVariant = Color(0xFFD8C2BC)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB5A0)
private val md_theme_dark_onPrimary = Color(0xFF5F1500)
private val md_theme_dark_primaryContainer = Color(0xFF862200)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBD1)
private val md_theme_dark_secondary = Color(0xFFE7BDB2)
private val md_theme_dark_onSecondary = Color(0xFF442A22)
private val md_theme_dark_secondaryContainer = Color(0xFF5D4037)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBD1)
private val md_theme_dark_tertiary = Color(0xFFD8C58D)
private val md_theme_dark_onTertiary = Color(0xFF3B2F05)
private val md_theme_dark_tertiaryContainer = Color(0xFF534619)
private val md_theme_dark_onTertiaryContainer = Color(0xFFF5E1A7)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201A18)
private val md_theme_dark_onBackground = Color(0xFFEDE0DC)
private val md_theme_dark_surface = Color(0xFF201A18)
private val md_theme_dark_onSurface = Color(0xFFEDE0DC)
private val md_theme_dark_surfaceVariant = Color(0xFF53433F)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BC)
private val md_theme_dark_outline = Color(0xFFA08C87)
private val md_theme_dark_inverseOnSurface = Color(0xFF201A18)
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DC)
private val md_theme_dark_inversePrimary = Color(0xFFB02F00)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB5A0)
private val md_theme_dark_outlineVariant = Color(0xFF53433F)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightDeepOrangeTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkDeepOrangeTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF6F43C0)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFEBDDFF)
private val md_theme_light_onPrimaryContainer = Color(0xFF250059)
private val md_theme_light_secondary = Color(0xFF635B70)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFE9DEF8)
private val md_theme_light_onSecondaryContainer = Color(0xFF1F182B)
private val md_theme_light_tertiary = Color(0xFF7E525D)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFFD9E1)
private val md_theme_light_onTertiaryContainer = Color(0xFF31101B)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF1D1B1E)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF1D1B1E)
private val md_theme_light_surfaceVariant = Color(0xFFE7E0EB)
private val md_theme_light_onSurfaceVariant = Color(0xFF49454E)
private val md_theme_light_outline = Color(0xFF7A757F)
private val md_theme_light_inverseOnSurface = Color(0xFFF5EFF4)
private val md_theme_light_inverseSurface = Color(0xFF323033)
private val md_theme_light_inversePrimary = Color(0xFFD3BBFF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF6F43C0)
private val md_theme_light_outlineVariant = Color(0xFFCBC4CF)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFD3BBFF)
private val md_theme_dark_onPrimary = Color(0xFF3F008D)
private val md_theme_dark_primaryContainer = Color(0xFF5727A6)
private val md_theme_dark_onPrimaryContainer = Color(0xFFEBDDFF)
private val md_theme_dark_secondary = Color(0xFFCDC2DB)
private val md_theme_dark_onSecondary = Color(0xFF342D40)
private val md_theme_dark_secondaryContainer = Color(0xFF4B4358)
private val md_theme_dark_onSecondaryContainer = Color(0xFFE9DEF8)
private val md_theme_dark_tertiary = Color(0xFFF0B7C5)
private val md_theme_dark_onTertiary = Color(0xFF4A2530)
private val md_theme_dark_tertiaryContainer = Color(0xFF643B46)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E1)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1D1B1E)
private val md_theme_dark_onBackground = Color(0xFFE6E1E6)
private val md_theme_dark_surface = Color(0xFF1D1B1E)
private val md_theme_dark_onSurface = Color(0xFFE6E1E6)
private val md_theme_dark_surfaceVariant = Color(0xFF49454E)
private val md_theme_dark_onSurfaceVariant = Color(0xFFCBC4CF)
private val md_theme_dark_outline = Color(0xFF948F99)
private val md_theme_dark_inverseOnSurface = Color(0xFF1D1B1E)
private val md_theme_dark_inverseSurface = Color(0xFFE6E1E6)
private val md_theme_dark_inversePrimary = Color(0xFF6F43C0)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFD3BBFF)
private val md_theme_dark_outlineVariant = Color(0xFF49454E)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightDeepPurpleTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkDeepPurpleTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF006E1A)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFF96F990)
private val md_theme_light_onPrimaryContainer = Color(0xFF002203)
private val md_theme_light_secondary = Color(0xFF53634F)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFD6E8CE)
private val md_theme_light_onSecondaryContainer = Color(0xFF111F0F)
private val md_theme_light_tertiary = Color(0xFF38656A)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFBCEBF0)
private val md_theme_light_onTertiaryContainer = Color(0xFF002023)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFCFDF6)
private val md_theme_light_onBackground = Color(0xFF1A1C19)
private val md_theme_light_surface = Color(0xFFFCFDF6)
private val md_theme_light_onSurface = Color(0xFF1A1C19)
private val md_theme_light_surfaceVariant = Color(0xFFDEE5D8)
private val md_theme_light_onSurfaceVariant = Color(0xFF424940)
private val md_theme_light_outline = Color(0xFF72796F)
private val md_theme_light_inverseOnSurface = Color(0xFFF1F1EB)
private val md_theme_light_inverseSurface = Color(0xFF2F312D)
private val md_theme_light_inversePrimary = Color(0xFF7ADC77)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF006E1A)
private val md_theme_light_outlineVariant = Color(0xFFC2C8BD)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF7ADC77)
private val md_theme_dark_onPrimary = Color(0xFF003909)
private val md_theme_dark_primaryContainer = Color(0xFF005311)
private val md_theme_dark_onPrimaryContainer = Color(0xFF96F990)
private val md_theme_dark_secondary = Color(0xFFBACCB3)
private val md_theme_dark_onSecondary = Color(0xFF253423)
private val md_theme_dark_secondaryContainer = Color(0xFF3B4B38)
private val md_theme_dark_onSecondaryContainer = Color(0xFFD6E8CE)
private val md_theme_dark_tertiary = Color(0xFFA0CFD4)
private val md_theme_dark_onTertiary = Color(0xFF00363B)
private val md_theme_dark_tertiaryContainer = Color(0xFF1E4D52)
private val md_theme_dark_onTertiaryContainer = Color(0xFFBCEBF0)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1A1C19)
private val md_theme_dark_onBackground = Color(0xFFE2E3DD)
private val md_theme_dark_surface = Color(0xFF1A1C19)
private val md_theme_dark_onSurface = Color(0xFFE2E3DD)
private val md_theme_dark_surfaceVariant = Color(0xFF424940)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC2C8BD)
private val md_theme_dark_outline = Color(0xFF8C9388)
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19)
private val md_theme_dark_inverseSurface = Color(0xFFE2E3DD)
private val md_theme_dark_inversePrimary = Color(0xFF006E1A)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF7ADC77)
private val md_theme_dark_outlineVariant = Color(0xFF424940)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightGreenTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkGreenTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF4355B9)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFDEE0FF)
private val md_theme_light_onPrimaryContainer = Color(0xFF00105C)
private val md_theme_light_secondary = Color(0xFF5B5D72)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFE0E1F9)
private val md_theme_light_onSecondaryContainer = Color(0xFF181A2C)
private val md_theme_light_tertiary = Color(0xFF77536D)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFFD7F1)
private val md_theme_light_onTertiaryContainer = Color(0xFF2D1228)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFEFBFF)
private val md_theme_light_onBackground = Color(0xFF1B1B1F)
private val md_theme_light_surface = Color(0xFFFEFBFF)
private val md_theme_light_onSurface = Color(0xFF1B1B1F)
private val md_theme_light_surfaceVariant = Color(0xFFE3E1EC)
private val md_theme_light_onSurfaceVariant = Color(0xFF46464F)
private val md_theme_light_outline = Color(0xFF767680)
private val md_theme_light_inverseOnSurface = Color(0xFFF3F0F4)
private val md_theme_light_inverseSurface = Color(0xFF303034)
private val md_theme_light_inversePrimary = Color(0xFFBAC3FF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF4355B9)
private val md_theme_light_outlineVariant = Color(0xFFC7C5D0)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFBAC3FF)
private val md_theme_dark_onPrimary = Color(0xFF08218A)
private val md_theme_dark_primaryContainer = Color(0xFF293CA0)
private val md_theme_dark_onPrimaryContainer = Color(0xFFDEE0FF)
private val md_theme_dark_secondary = Color(0xFFC3C5DD)
private val md_theme_dark_onSecondary = Color(0xFF2D2F42)
private val md_theme_dark_secondaryContainer = Color(0xFF434659)
private val md_theme_dark_onSecondaryContainer = Color(0xFFE0E1F9)
private val md_theme_dark_tertiary = Color(0xFFE6BAD7)
private val md_theme_dark_onTertiary = Color(0xFF44263D)
private val md_theme_dark_tertiaryContainer = Color(0xFF5D3C55)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFD7F1)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1B1B1F)
private val md_theme_dark_onBackground = Color(0xFFE4E1E6)
private val md_theme_dark_surface = Color(0xFF1B1B1F)
private val md_theme_dark_onSurface = Color(0xFFE4E1E6)
private val md_theme_dark_surfaceVariant = Color(0xFF46464F)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC7C5D0)
private val md_theme_dark_outline = Color(0xFF90909A)
private val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
private val md_theme_dark_inverseSurface = Color(0xFFE4E1E6)
private val md_theme_dark_inversePrimary = Color(0xFF4355B9)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFBAC3FF)
private val md_theme_dark_outlineVariant = Color(0xFF46464F)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightIndigoTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkIndigoTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,132 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF006493)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFCAE6FF)
private val md_theme_light_onPrimaryContainer = Color(0xFF001E30)
private val md_theme_light_secondary = Color(0xFF50606E)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFD3E5F5)
private val md_theme_light_onSecondaryContainer = Color(0xFF0C1D29)
private val md_theme_light_tertiary = Color(0xFF65587B)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFEBDDFF)
private val md_theme_light_onTertiaryContainer = Color(0xFF201634)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFCFCFF)
private val md_theme_light_onBackground = Color(0xFF1A1C1E)
private val md_theme_light_surface = Color(0xFFFCFCFF)
private val md_theme_light_onSurface = Color(0xFF1A1C1E)
private val md_theme_light_surfaceVariant = Color(0xFFDDE3EA)
private val md_theme_light_onSurfaceVariant = Color(0xFF41474D)
private val md_theme_light_outline = Color(0xFF72787E)
private val md_theme_light_inverseOnSurface = Color(0xFFF0F0F3)
private val md_theme_light_inverseSurface = Color(0xFF2E3133)
private val md_theme_light_inversePrimary = Color(0xFF8DCDFF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF006493)
private val md_theme_light_outlineVariant = Color(0xFFC1C7CE)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF8DCDFF)
private val md_theme_dark_onPrimary = Color(0xFF00344F)
private val md_theme_dark_primaryContainer = Color(0xFF004B70)
private val md_theme_dark_onPrimaryContainer = Color(0xFFCAE6FF)
private val md_theme_dark_secondary = Color(0xFFB7C9D9)
private val md_theme_dark_onSecondary = Color(0xFF22323F)
private val md_theme_dark_secondaryContainer = Color(0xFF384956)
private val md_theme_dark_onSecondaryContainer = Color(0xFFD3E5F5)
private val md_theme_dark_tertiary = Color(0xFFCFC0E8)
private val md_theme_dark_onTertiary = Color(0xFF362B4B)
private val md_theme_dark_tertiaryContainer = Color(0xFF4D4162)
private val md_theme_dark_onTertiaryContainer = Color(0xFFEBDDFF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1A1C1E)
private val md_theme_dark_onBackground = Color(0xFFE2E2E5)
private val md_theme_dark_surface = Color(0xFF1A1C1E)
private val md_theme_dark_onSurface = Color(0xFFE2E2E5)
private val md_theme_dark_surfaceVariant = Color(0xFF41474D)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE)
private val md_theme_dark_outline = Color(0xFF8B9198)
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E)
private val md_theme_dark_inverseSurface = Color(0xFFE2E2E5)
private val md_theme_dark_inversePrimary = Color(0xFF006493)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF8DCDFF)
private val md_theme_dark_outlineVariant = Color(0xFF41474D)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightLightBlueTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkLightBlueTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF006C48)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFF8DF7C2)
private val md_theme_light_onPrimaryContainer = Color(0xFF002113)
private val md_theme_light_secondary = Color(0xFF4D6356)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFD0E8D8)
private val md_theme_light_onSecondaryContainer = Color(0xFF0A1F15)
private val md_theme_light_tertiary = Color(0xFF3C6472)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFC0E9FA)
private val md_theme_light_onTertiaryContainer = Color(0xFF001F28)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFBFDF8)
private val md_theme_light_onBackground = Color(0xFF191C1A)
private val md_theme_light_surface = Color(0xFFFBFDF8)
private val md_theme_light_onSurface = Color(0xFF191C1A)
private val md_theme_light_surfaceVariant = Color(0xFFDCE5DD)
private val md_theme_light_onSurfaceVariant = Color(0xFF404943)
private val md_theme_light_outline = Color(0xFF707973)
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
private val md_theme_light_inverseSurface = Color(0xFF2E312F)
private val md_theme_light_inversePrimary = Color(0xFF70DBA7)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF006C48)
private val md_theme_light_outlineVariant = Color(0xFFC0C9C1)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF70DBA7)
private val md_theme_dark_onPrimary = Color(0xFF003824)
private val md_theme_dark_primaryContainer = Color(0xFF005235)
private val md_theme_dark_onPrimaryContainer = Color(0xFF8DF7C2)
private val md_theme_dark_secondary = Color(0xFFB4CCBC)
private val md_theme_dark_onSecondary = Color(0xFF20352A)
private val md_theme_dark_secondaryContainer = Color(0xFF364B3F)
private val md_theme_dark_onSecondaryContainer = Color(0xFFD0E8D8)
private val md_theme_dark_tertiary = Color(0xFFA4CDDE)
private val md_theme_dark_onTertiary = Color(0xFF063543)
private val md_theme_dark_tertiaryContainer = Color(0xFF234C5A)
private val md_theme_dark_onTertiaryContainer = Color(0xFFC0E9FA)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF191C1A)
private val md_theme_dark_onBackground = Color(0xFFE1E3DF)
private val md_theme_dark_surface = Color(0xFF191C1A)
private val md_theme_dark_onSurface = Color(0xFFE1E3DF)
private val md_theme_dark_surfaceVariant = Color(0xFF404943)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC0C9C1)
private val md_theme_dark_outline = Color(0xFF8A938C)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
private val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
private val md_theme_dark_inversePrimary = Color(0xFF006C48)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF70DBA7)
private val md_theme_dark_outlineVariant = Color(0xFF404943)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightLightGreenTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkLightGreenTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF5B6300)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFDDED49)
private val md_theme_light_onPrimaryContainer = Color(0xFF1A1D00)
private val md_theme_light_secondary = Color(0xFF5E6044)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFE4E5C1)
private val md_theme_light_onSecondaryContainer = Color(0xFF1B1D07)
private val md_theme_light_tertiary = Color(0xFF3C665A)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFBEECDC)
private val md_theme_light_onTertiaryContainer = Color(0xFF002019)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFEFFD8)
private val md_theme_light_onBackground = Color(0xFF1C1C17)
private val md_theme_light_surface = Color(0xFFFEFFD8)
private val md_theme_light_onSurface = Color(0xFF1C1C17)
private val md_theme_light_surfaceVariant = Color(0xFFE5E3D2)
private val md_theme_light_onSurfaceVariant = Color(0xFF47483B)
private val md_theme_light_outline = Color(0xFF787869)
private val md_theme_light_inverseOnSurface = Color(0xFFF3F1E8)
private val md_theme_light_inverseSurface = Color(0xFF31312B)
private val md_theme_light_inversePrimary = Color(0xFFC1D02C)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF5B6300)
private val md_theme_light_outlineVariant = Color(0xFFC8C7B7)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFC1D02C)
private val md_theme_dark_onPrimary = Color(0xFF2F3300)
private val md_theme_dark_primaryContainer = Color(0xFF444B00)
private val md_theme_dark_onPrimaryContainer = Color(0xFFDDED49)
private val md_theme_dark_secondary = Color(0xFFC7C9A6)
private val md_theme_dark_onSecondary = Color(0xFF30321A)
private val md_theme_dark_secondaryContainer = Color(0xFF46492E)
private val md_theme_dark_onSecondaryContainer = Color(0xFFE4E5C1)
private val md_theme_dark_tertiary = Color(0xFFA2D0C1)
private val md_theme_dark_onTertiary = Color(0xFF07372D)
private val md_theme_dark_tertiaryContainer = Color(0xFF234E43)
private val md_theme_dark_onTertiaryContainer = Color(0xFFBEECDC)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1C1C17)
private val md_theme_dark_onBackground = Color(0xFFE5E2DA)
private val md_theme_dark_surface = Color(0xFF1C1C17)
private val md_theme_dark_onSurface = Color(0xFFE5E2DA)
private val md_theme_dark_surfaceVariant = Color(0xFF47483B)
private val md_theme_dark_onSurfaceVariant = Color(0xFFC8C7B7)
private val md_theme_dark_outline = Color(0xFF929282)
private val md_theme_dark_inverseOnSurface = Color(0xFF1C1C17)
private val md_theme_dark_inverseSurface = Color(0xFFE5E2DA)
private val md_theme_dark_inversePrimary = Color(0xFF5B6300)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFC1D02C)
private val md_theme_dark_outlineVariant = Color(0xFF47483B)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightLimeTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkLimeTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF8B5000)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFDCBE)
private val md_theme_light_onPrimaryContainer = Color(0xFF2C1600)
private val md_theme_light_secondary = Color(0xFF725A42)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFDCBE)
private val md_theme_light_onSecondaryContainer = Color(0xFF291806)
private val md_theme_light_tertiary = Color(0xFF58633A)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFDCE8B4)
private val md_theme_light_onTertiaryContainer = Color(0xFF161E01)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201B16)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201B16)
private val md_theme_light_surfaceVariant = Color(0xFFF2DFD1)
private val md_theme_light_onSurfaceVariant = Color(0xFF51453A)
private val md_theme_light_outline = Color(0xFF837468)
private val md_theme_light_inverseOnSurface = Color(0xFFFAEFE7)
private val md_theme_light_inverseSurface = Color(0xFF352F2B)
private val md_theme_light_inversePrimary = Color(0xFFFFB870)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF8B5000)
private val md_theme_light_outlineVariant = Color(0xFFD5C3B5)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB870)
private val md_theme_dark_onPrimary = Color(0xFF4A2800)
private val md_theme_dark_primaryContainer = Color(0xFF693C00)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDCBE)
private val md_theme_dark_secondary = Color(0xFFE1C1A4)
private val md_theme_dark_onSecondary = Color(0xFF402C18)
private val md_theme_dark_secondaryContainer = Color(0xFF59422C)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCBE)
private val md_theme_dark_tertiary = Color(0xFFC0CC9A)
private val md_theme_dark_onTertiary = Color(0xFF2B3410)
private val md_theme_dark_tertiaryContainer = Color(0xFF414B24)
private val md_theme_dark_onTertiaryContainer = Color(0xFFDCE8B4)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201B16)
private val md_theme_dark_onBackground = Color(0xFFEBE0D9)
private val md_theme_dark_surface = Color(0xFF201B16)
private val md_theme_dark_onSurface = Color(0xFFEBE0D9)
private val md_theme_dark_surfaceVariant = Color(0xFF51453A)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD5C3B5)
private val md_theme_dark_outline = Color(0xFF9D8E81)
private val md_theme_dark_inverseOnSurface = Color(0xFF201B16)
private val md_theme_dark_inverseSurface = Color(0xFFEBE0D9)
private val md_theme_dark_inversePrimary = Color(0xFF8B5000)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB870)
private val md_theme_dark_outlineVariant = Color(0xFF51453A)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightOrangeTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkOrangeTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFFBC004B)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFD9DE)
private val md_theme_light_onPrimaryContainer = Color(0xFF400014)
private val md_theme_light_secondary = Color(0xFF75565B)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFD9DE)
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1519)
private val md_theme_light_tertiary = Color(0xFF795831)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFFDDBA)
private val md_theme_light_onTertiaryContainer = Color(0xFF2B1700)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201A1B)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201A1B)
private val md_theme_light_surfaceVariant = Color(0xFFF3DDDF)
private val md_theme_light_onSurfaceVariant = Color(0xFF524345)
private val md_theme_light_outline = Color(0xFF847375)
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
private val md_theme_light_inverseSurface = Color(0xFF362F2F)
private val md_theme_light_inversePrimary = Color(0xFFFFB2BE)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFFBC004B)
private val md_theme_light_outlineVariant = Color(0xFFD6C2C3)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB2BE)
private val md_theme_dark_onPrimary = Color(0xFF660025)
private val md_theme_dark_primaryContainer = Color(0xFF900038)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9DE)
private val md_theme_dark_secondary = Color(0xFFE5BDC2)
private val md_theme_dark_onSecondary = Color(0xFF43292D)
private val md_theme_dark_secondaryContainer = Color(0xFF5C3F43)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9DE)
private val md_theme_dark_tertiary = Color(0xFFEBBF90)
private val md_theme_dark_onTertiary = Color(0xFF452B08)
private val md_theme_dark_tertiaryContainer = Color(0xFF5F411C)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDBA)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201A1B)
private val md_theme_dark_onBackground = Color(0xFFECE0E0)
private val md_theme_dark_surface = Color(0xFF201A1B)
private val md_theme_dark_onSurface = Color(0xFFECE0E0)
private val md_theme_dark_surfaceVariant = Color(0xFF524345)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD6C2C3)
private val md_theme_dark_outline = Color(0xFF9F8C8E)
private val md_theme_dark_inverseOnSurface = Color(0xFF201A1B)
private val md_theme_dark_inverseSurface = Color(0xFFECE0E0)
private val md_theme_dark_inversePrimary = Color(0xFFBC004B)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB2BE)
private val md_theme_dark_outlineVariant = Color(0xFF524345)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightPinkTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkPinkTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF9A25AE)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFD6FE)
private val md_theme_light_onPrimaryContainer = Color(0xFF35003F)
private val md_theme_light_secondary = Color(0xFF6B586B)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFF4DBF1)
private val md_theme_light_onSecondaryContainer = Color(0xFF251626)
private val md_theme_light_tertiary = Color(0xFF82524A)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFFDAD4)
private val md_theme_light_onTertiaryContainer = Color(0xFF33110C)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF1E1A1D)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF1E1A1D)
private val md_theme_light_surfaceVariant = Color(0xFFECDFE8)
private val md_theme_light_onSurfaceVariant = Color(0xFF4D444C)
private val md_theme_light_outline = Color(0xFF7F747D)
private val md_theme_light_inverseOnSurface = Color(0xFFF7EEF3)
private val md_theme_light_inverseSurface = Color(0xFF332F32)
private val md_theme_light_inversePrimary = Color(0xFFF9ABFF)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF9A25AE)
private val md_theme_light_outlineVariant = Color(0xFFD0C3CC)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFF9ABFF)
private val md_theme_dark_onPrimary = Color(0xFF570066)
private val md_theme_dark_primaryContainer = Color(0xFF7B008F)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD6FE)
private val md_theme_dark_secondary = Color(0xFFD7BFD5)
private val md_theme_dark_onSecondary = Color(0xFF3B2B3C)
private val md_theme_dark_secondaryContainer = Color(0xFF534153)
private val md_theme_dark_onSecondaryContainer = Color(0xFFF4DBF1)
private val md_theme_dark_tertiary = Color(0xFFF6B8AD)
private val md_theme_dark_onTertiary = Color(0xFF4C251F)
private val md_theme_dark_tertiaryContainer = Color(0xFF673B34)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDAD4)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1E1A1D)
private val md_theme_dark_onBackground = Color(0xFFE9E0E4)
private val md_theme_dark_surface = Color(0xFF1E1A1D)
private val md_theme_dark_onSurface = Color(0xFFE9E0E4)
private val md_theme_dark_surfaceVariant = Color(0xFF4D444C)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD0C3CC)
private val md_theme_dark_outline = Color(0xFF998D96)
private val md_theme_dark_inverseOnSurface = Color(0xFF1E1A1D)
private val md_theme_dark_inverseSurface = Color(0xFFE9E0E4)
private val md_theme_dark_inversePrimary = Color(0xFF9A25AE)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFF9ABFF)
private val md_theme_dark_outlineVariant = Color(0xFF4D444C)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightPurpleTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkPurpleTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFFBB1614)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFDAD5)
private val md_theme_light_onPrimaryContainer = Color(0xFF410001)
private val md_theme_light_secondary = Color(0xFF775652)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFDAD5)
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1512)
private val md_theme_light_tertiary = Color(0xFF705C2E)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFCDFA6)
private val md_theme_light_onTertiaryContainer = Color(0xFF261A00)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201A19)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201A19)
private val md_theme_light_surfaceVariant = Color(0xFFF5DDDA)
private val md_theme_light_onSurfaceVariant = Color(0xFF534341)
private val md_theme_light_outline = Color(0xFF857370)
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEC)
private val md_theme_light_inverseSurface = Color(0xFF362F2E)
private val md_theme_light_inversePrimary = Color(0xFFFFB4A9)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFFBB1614)
private val md_theme_light_outlineVariant = Color(0xFFD8C2BE)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB4A9)
private val md_theme_dark_onPrimary = Color(0xFF690002)
private val md_theme_dark_primaryContainer = Color(0xFF930005)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD5)
private val md_theme_dark_secondary = Color(0xFFE7BDB7)
private val md_theme_dark_onSecondary = Color(0xFF442926)
private val md_theme_dark_secondaryContainer = Color(0xFF5D3F3B)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD5)
private val md_theme_dark_tertiary = Color(0xFFDFC38C)
private val md_theme_dark_onTertiary = Color(0xFF3E2E04)
private val md_theme_dark_tertiaryContainer = Color(0xFF574419)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFCDFA6)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201A19)
private val md_theme_dark_onBackground = Color(0xFFEDE0DE)
private val md_theme_dark_surface = Color(0xFF201A19)
private val md_theme_dark_onSurface = Color(0xFFEDE0DE)
private val md_theme_dark_surfaceVariant = Color(0xFF534341)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BE)
private val md_theme_dark_outline = Color(0xFFA08C89)
private val md_theme_dark_inverseOnSurface = Color(0xFF201A19)
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DE)
private val md_theme_dark_inversePrimary = Color(0xFFBB1614)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB4A9)
private val md_theme_dark_outlineVariant = Color(0xFF534341)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightRedTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkRedTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF9B404F)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFFFD9DC)
private val md_theme_light_onPrimaryContainer = Color(0xFF400011)
private val md_theme_light_secondary = Color(0xFF765659)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFFFD9DC)
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1518)
private val md_theme_light_tertiary = Color(0xFF785830)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFFFDDB7)
private val md_theme_light_onTertiaryContainer = Color(0xFF2A1700)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF201A1A)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF201A1A)
private val md_theme_light_surfaceVariant = Color(0xFFF4DDDE)
private val md_theme_light_onSurfaceVariant = Color(0xFF524344)
private val md_theme_light_outline = Color(0xFF847374)
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
private val md_theme_light_inverseSurface = Color(0xFF362F2F)
private val md_theme_light_inversePrimary = Color(0xFFFFB2BA)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF9B404F)
private val md_theme_light_outlineVariant = Color(0xFFD7C1C3)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFFFB2BA)
private val md_theme_dark_onPrimary = Color(0xFF5F1223)
private val md_theme_dark_primaryContainer = Color(0xFF7D2939)
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9DC)
private val md_theme_dark_secondary = Color(0xFFE5BDC0)
private val md_theme_dark_onSecondary = Color(0xFF43292C)
private val md_theme_dark_secondaryContainer = Color(0xFF5C3F42)
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9DC)
private val md_theme_dark_tertiary = Color(0xFFE9BF8F)
private val md_theme_dark_onTertiary = Color(0xFF442B07)
private val md_theme_dark_tertiaryContainer = Color(0xFF5E411B)
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB7)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF201A1A)
private val md_theme_dark_onBackground = Color(0xFFECE0E0)
private val md_theme_dark_surface = Color(0xFF201A1A)
private val md_theme_dark_onSurface = Color(0xFFECE0E0)
private val md_theme_dark_surfaceVariant = Color(0xFF524344)
private val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C3)
private val md_theme_dark_outline = Color(0xFF9F8C8D)
private val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
private val md_theme_dark_inverseSurface = Color(0xFFECE0E0)
private val md_theme_dark_inversePrimary = Color(0xFF9B404F)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFFFB2BA)
private val md_theme_dark_outlineVariant = Color(0xFF524344)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightSakuraTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkSakuraTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,132 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF006A60)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFF74F8E5)
private val md_theme_light_onPrimaryContainer = Color(0xFF00201C)
private val md_theme_light_secondary = Color(0xFF4A635F)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFCCE8E2)
private val md_theme_light_onSecondaryContainer = Color(0xFF05201C)
private val md_theme_light_tertiary = Color(0xFF456179)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFCCE5FF)
private val md_theme_light_onTertiaryContainer = Color(0xFF001E31)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFAFDFB)
private val md_theme_light_onBackground = Color(0xFF191C1B)
private val md_theme_light_surface = Color(0xFFFAFDFB)
private val md_theme_light_onSurface = Color(0xFF191C1B)
private val md_theme_light_surfaceVariant = Color(0xFFDAE5E1)
private val md_theme_light_onSurfaceVariant = Color(0xFF3F4947)
private val md_theme_light_outline = Color(0xFF6F7977)
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1EF)
private val md_theme_light_inverseSurface = Color(0xFF2D3130)
private val md_theme_light_inversePrimary = Color(0xFF53DBC9)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF006A60)
private val md_theme_light_outlineVariant = Color(0xFFBEC9C6)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF53DBC9)
private val md_theme_dark_onPrimary = Color(0xFF003731)
private val md_theme_dark_primaryContainer = Color(0xFF005048)
private val md_theme_dark_onPrimaryContainer = Color(0xFF74F8E5)
private val md_theme_dark_secondary = Color(0xFFB1CCC6)
private val md_theme_dark_onSecondary = Color(0xFF1C3531)
private val md_theme_dark_secondaryContainer = Color(0xFF334B47)
private val md_theme_dark_onSecondaryContainer = Color(0xFFCCE8E2)
private val md_theme_dark_tertiary = Color(0xFFADCAE6)
private val md_theme_dark_onTertiary = Color(0xFF153349)
private val md_theme_dark_tertiaryContainer = Color(0xFF2D4961)
private val md_theme_dark_onTertiaryContainer = Color(0xFFCCE5FF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF191C1B)
private val md_theme_dark_onBackground = Color(0xFFE0E3E1)
private val md_theme_dark_surface = Color(0xFF191C1B)
private val md_theme_dark_onSurface = Color(0xFFE0E3E1)
private val md_theme_dark_surfaceVariant = Color(0xFF3F4947)
private val md_theme_dark_onSurfaceVariant = Color(0xFFBEC9C6)
private val md_theme_dark_outline = Color(0xFF899390)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1B)
private val md_theme_dark_inverseSurface = Color(0xFFE0E3E1)
private val md_theme_dark_inversePrimary = Color(0xFF006A60)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFF53DBC9)
private val md_theme_dark_outlineVariant = Color(0xFF3F4947)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightTealTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkTealTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,172 @@
package me.bmax.apatch.ui.theme
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.MutableLiveData
import me.bmax.apatch.APApplication
@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(),
)
}
)
}
}
val refreshTheme = MutableLiveData(false)
@Composable
fun APatchTheme(
content: @Composable () -> Unit
) {
val context = LocalContext.current
val prefs = APApplication.sharedPreferences
var darkThemeFollowSys by remember {
mutableStateOf(
prefs.getBoolean(
"night_mode_follow_sys",
true
)
)
}
var nightModeEnabled by remember {
mutableStateOf(
prefs.getBoolean(
"night_mode_enabled",
false
)
)
}
// Dynamic color is available on Android 12+, and custom 1t!
var dynamicColor by remember {
mutableStateOf(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) prefs.getBoolean(
"use_system_color_theme",
true
) else false
)
}
var customColorScheme by remember { mutableStateOf(prefs.getString("custom_color", "blue")) }
val refreshThemeObserver by refreshTheme.observeAsState(false)
if (refreshThemeObserver == true) {
darkThemeFollowSys = prefs.getBoolean("night_mode_follow_sys", true)
nightModeEnabled = prefs.getBoolean("night_mode_enabled", false)
dynamicColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) prefs.getBoolean(
"use_system_color_theme",
true
) else false
customColorScheme = prefs.getString("custom_color", "blue")
refreshTheme.postValue(false)
}
val darkTheme = if (darkThemeFollowSys) {
isSystemInDarkTheme()
} else {
nightModeEnabled
}
val colorScheme = if (!dynamicColor) {
if (darkTheme) {
when (customColorScheme) {
"amber" -> DarkAmberTheme
"blue_grey" -> DarkBlueGreyTheme
"blue" -> DarkBlueTheme
"brown" -> DarkBrownTheme
"cyan" -> DarkCyanTheme
"deep_orange" -> DarkDeepOrangeTheme
"deep_purple" -> DarkDeepPurpleTheme
"green" -> DarkGreenTheme
"indigo" -> DarkIndigoTheme
"light_blue" -> DarkLightBlueTheme
"light_green" -> DarkLightGreenTheme
"lime" -> DarkLimeTheme
"orange" -> DarkOrangeTheme
"pink" -> DarkPinkTheme
"purple" -> DarkPurpleTheme
"red" -> DarkRedTheme
"sakura" -> DarkSakuraTheme
"teal" -> DarkTealTheme
"yellow" -> DarkYellowTheme
else -> DarkBlueTheme
}
} else {
when (customColorScheme) {
"amber" -> LightAmberTheme
"blue_grey" -> LightBlueGreyTheme
"blue" -> LightBlueTheme
"brown" -> LightBrownTheme
"cyan" -> LightCyanTheme
"deep_orange" -> LightDeepOrangeTheme
"deep_purple" -> LightDeepPurpleTheme
"green" -> LightGreenTheme
"indigo" -> LightIndigoTheme
"light_blue" -> LightLightBlueTheme
"light_green" -> LightLightGreenTheme
"lime" -> LightLimeTheme
"orange" -> LightOrangeTheme
"pink" -> LightPinkTheme
"purple" -> LightPurpleTheme
"red" -> LightRedTheme
"sakura" -> LightSakuraTheme
"teal" -> LightTealTheme
"yellow" -> LightYellowTheme
else -> LightBlueTheme
}
}
} else {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkBlueTheme
else -> LightBlueTheme
}
}
SystemBarStyle(
darkMode = darkTheme
)
MaterialTheme(
colorScheme = colorScheme, typography = Typography, content = content
)
}

View file

@ -0,0 +1,33 @@
package me.bmax.apatch.ui.theme
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = androidx.compose.material3.Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View file

@ -0,0 +1,131 @@
package me.bmax.apatch.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
private val md_theme_light_primary = Color(0xFF695F00)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFF9E534)
private val md_theme_light_onPrimaryContainer = Color(0xFF201C00)
private val md_theme_light_secondary = Color(0xFF645F41)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFEBE3BD)
private val md_theme_light_onSecondaryContainer = Color(0xFF1F1C05)
private val md_theme_light_tertiary = Color(0xFF406652)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFC2ECD3)
private val md_theme_light_onTertiaryContainer = Color(0xFF002113)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFFFBFF)
private val md_theme_light_onBackground = Color(0xFF1D1C16)
private val md_theme_light_surface = Color(0xFFFFFBFF)
private val md_theme_light_onSurface = Color(0xFF1D1C16)
private val md_theme_light_surfaceVariant = Color(0xFFE8E2D0)
private val md_theme_light_onSurfaceVariant = Color(0xFF4A473A)
private val md_theme_light_outline = Color(0xFF7B7768)
private val md_theme_light_inverseOnSurface = Color(0xFFF5F0E7)
private val md_theme_light_inverseSurface = Color(0xFF32302A)
private val md_theme_light_inversePrimary = Color(0xFFDBC90A)
private val md_theme_light_shadow = Color(0xFF000000)
private val md_theme_light_surfaceTint = Color(0xFF695F00)
private val md_theme_light_outlineVariant = Color(0xFFCBC6B5)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFFDBC90A)
private val md_theme_dark_onPrimary = Color(0xFF363100)
private val md_theme_dark_primaryContainer = Color(0xFF4F4800)
private val md_theme_dark_onPrimaryContainer = Color(0xFFF9E534)
private val md_theme_dark_secondary = Color(0xFFCEC7A3)
private val md_theme_dark_onSecondary = Color(0xFF343117)
private val md_theme_dark_secondaryContainer = Color(0xFF4B472B)
private val md_theme_dark_onSecondaryContainer = Color(0xFFEBE3BD)
private val md_theme_dark_tertiary = Color(0xFFA7D0B7)
private val md_theme_dark_onTertiary = Color(0xFF103726)
private val md_theme_dark_tertiaryContainer = Color(0xFF294E3B)
private val md_theme_dark_onTertiaryContainer = Color(0xFFC2ECD3)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF1D1C16)
private val md_theme_dark_onBackground = Color(0xFFE7E2D9)
private val md_theme_dark_surface = Color(0xFF1D1C16)
private val md_theme_dark_onSurface = Color(0xFFE7E2D9)
private val md_theme_dark_surfaceVariant = Color(0xFF4A473A)
private val md_theme_dark_onSurfaceVariant = Color(0xFFCBC6B5)
private val md_theme_dark_outline = Color(0xFF959181)
private val md_theme_dark_inverseOnSurface = Color(0xFF1D1C16)
private val md_theme_dark_inverseSurface = Color(0xFFE7E2D9)
private val md_theme_dark_inversePrimary = Color(0xFF695F00)
private val md_theme_dark_shadow = Color(0xFF000000)
private val md_theme_dark_surfaceTint = Color(0xFFDBC90A)
private val md_theme_dark_outlineVariant = Color(0xFF4A473A)
private val md_theme_dark_scrim = Color(0xFF000000)
val LightYellowTheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
val DarkYellowTheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)

View file

@ -0,0 +1,162 @@
package me.bmax.apatch.ui.viewmodel
import android.os.SystemClock
import android.util.Log
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.bmax.apatch.apApp
import me.bmax.apatch.util.listModules
import org.json.JSONArray
import org.json.JSONObject
import java.text.Collator
import java.util.Locale
class APModuleViewModel : ViewModel() {
companion object {
private const val TAG = "ModuleViewModel"
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
}
class ModuleInfo(
val id: String,
val name: String,
val author: String,
val version: String,
val versionCode: Int,
val description: String,
val enabled: Boolean,
val update: Boolean,
val remove: Boolean,
val updateJson: String,
val hasWebUi: Boolean,
val hasActionScript: Boolean,
)
data class ModuleUpdateInfo(
val version: String,
val versionCode: Int,
val zipUrl: String,
val changelog: String,
)
var isRefreshing by mutableStateOf(false)
private set
val moduleList by derivedStateOf {
val comparator = compareBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id)
modules.sortedWith(comparator).also {
isRefreshing = false
}
}
var isNeedRefresh by mutableStateOf(false)
private set
fun markNeedRefresh() {
isNeedRefresh = true
}
fun fetchModuleList() {
viewModelScope.launch(Dispatchers.IO) {
isRefreshing = true
val oldModuleList = modules
val start = SystemClock.elapsedRealtime()
kotlin.runCatching {
val result = listModules()
Log.i(TAG, "result: $result")
val array = JSONArray(result)
modules = (0 until array.length())
.asSequence()
.map { array.getJSONObject(it) }
.map { obj ->
ModuleInfo(
obj.getString("id"),
obj.optString("name"),
obj.optString("author", "Unknown"),
obj.optString("version", "Unknown"),
obj.optInt("versionCode", 0),
obj.optString("description"),
obj.getBoolean("enabled"),
obj.getBoolean("update"),
obj.getBoolean("remove"),
obj.optString("updateJson"),
obj.optBoolean("web"),
obj.optBoolean("action")
)
}.toList()
isNeedRefresh = false
}.onFailure { e ->
Log.e(TAG, "fetchModuleList: ", e)
isRefreshing = false
}
// when both old and new is kotlin.collections.EmptyList
// moduleList update will don't trigger
if (oldModuleList === modules) {
isRefreshing = false
}
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
}
}
private fun sanitizeVersionString(version: String): String {
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
}
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
val empty = Triple("", "", "")
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
return empty
}
// download updateJson
val result = kotlin.runCatching {
val url = m.updateJson
Log.i(TAG, "checkUpdate url: $url")
val response = apApp.okhttpClient
.newCall(
okhttp3.Request.Builder()
.url(url)
.build()
).execute()
Log.d(TAG, "checkUpdate code: ${response.code}")
if (response.isSuccessful) {
response.body?.string() ?: ""
} else {
""
}
}.getOrDefault("")
Log.i(TAG, "checkUpdate result: $result")
if (result.isEmpty()) {
return empty
}
val updateJson = kotlin.runCatching {
JSONObject(result)
}.getOrNull() ?: return empty
val version = sanitizeVersionString(updateJson.optString("version", ""))
val versionCode = updateJson.optInt("versionCode", 0)
val zipUrl = updateJson.optString("zipUrl", "")
val changelog = updateJson.optString("changelog", "")
if (versionCode <= m.versionCode || zipUrl.isEmpty()) {
return empty
}
return Triple(zipUrl, version, changelog)
}
}

View file

@ -0,0 +1,66 @@
package me.bmax.apatch.ui.viewmodel
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
object KPModel {
enum class TriggerEvent(val event: String) {
PAGING_INIT("paging-init"),
PRE_KERNEL_INIT("pre-kernel-init"),
POST_KERNEL_INIT("post-kernel-init"),
}
enum class ExtraType(val desc: String) {
NONE("none"),
KPM("kpm"),
SHELL("shell"),
EXEC("exec"),
RAW("raw"),
ANDROID_RC("android_rc");
}
interface IExtraInfo : Parcelable {
var type: ExtraType
var name: String
var event: String
var args: String
}
@Immutable
@Parcelize
@Keep
data class KPMInfo(
override var type: ExtraType,
override var name: String,
override var event: String,
override var args: String,
var version: String,
var license: String,
var author: String,
var description: String,
) : IExtraInfo
@Immutable
@Parcelize
@Keep
data class KPImgInfo(
var version: String,
var compileTime: String,
var config: String,
var superKey: String,
var rootSuperkey: String
) : Parcelable
@Immutable
@Parcelize
@Keep
data class KImgInfo(
var banner: String,
var patched: Boolean,
) : Parcelable
}

View file

@ -0,0 +1,91 @@
package me.bmax.apatch.ui.viewmodel
import android.os.SystemClock
import android.util.Log
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.bmax.apatch.Natives
import java.text.Collator
import java.util.Locale
class KPModuleViewModel : ViewModel() {
companion object {
private const val TAG = "KPModuleViewModel"
private var modules by mutableStateOf<List<KPModel.KPMInfo>>(emptyList())
}
var isRefreshing by mutableStateOf(false)
private set
val moduleList by derivedStateOf {
val comparator = compareBy(Collator.getInstance(Locale.getDefault()), KPModel.KPMInfo::name)
modules.sortedWith(comparator).also {
isRefreshing = false
}
}
var isNeedRefresh by mutableStateOf(false)
private set
fun markNeedRefresh() {
isNeedRefresh = true
}
fun fetchModuleList() {
viewModelScope.launch(Dispatchers.IO) {
isRefreshing = true
val oldModuleList = modules
val start = SystemClock.elapsedRealtime()
kotlin.runCatching {
var names = Natives.kernelPatchModuleList()
if (Natives.kernelPatchModuleNum() <= 0)
names = ""
val nameList = names.split('\n').toList()
Log.d(TAG, "kpm list: $nameList")
modules = nameList.filter { it.isNotEmpty() }.map {
val infoline = Natives.kernelPatchModuleInfo(it)
val spi = infoline.split('\n')
val name = spi.find { it.startsWith("name=") }?.removePrefix("name=")
val version = spi.find { it.startsWith("version=") }?.removePrefix("version=")
val license = spi.find { it.startsWith("license=") }?.removePrefix("license=")
val author = spi.find { it.startsWith("author=") }?.removePrefix("author=")
val description =
spi.find { it.startsWith("description=") }?.removePrefix("description=")
val args = spi.find { it.startsWith("args=") }?.removePrefix("args=")
val info = KPModel.KPMInfo(
KPModel.ExtraType.KPM,
name ?: "",
"",
args ?: "",
version ?: "",
license ?: "",
author ?: "",
description ?: ""
)
info
}
isNeedRefresh = false
}.onFailure { e ->
Log.e(TAG, "fetchModuleList: ", e)
isRefreshing = false
}
// when both old and new is kotlin.collections.EmptyList
// moduleList update will don't trigger
if (oldModuleList === modules) {
isRefreshing = false
}
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
}
}
}

View file

@ -0,0 +1,574 @@
package me.bmax.apatch.ui.viewmodel
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.system.Os
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.bmax.apatch.APApplication
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.R
import me.bmax.apatch.apApp
import me.bmax.apatch.util.Version
import me.bmax.apatch.util.copyAndClose
import me.bmax.apatch.util.copyAndCloseOut
import me.bmax.apatch.util.createRootShell
import me.bmax.apatch.util.inputStream
import me.bmax.apatch.util.shellForResult
import me.bmax.apatch.util.writeTo
import org.ini4j.Ini
import java.io.BufferedReader
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStreamReader
import java.io.StringReader
private const val TAG = "PatchViewModel"
class PatchesViewModel : ViewModel() {
enum class PatchMode(val sId: Int) {
PATCH_ONLY(R.string.patch_mode_bootimg_patch),
PATCH_AND_INSTALL(R.string.patch_mode_patch_and_install),
INSTALL_TO_NEXT_SLOT(R.string.patch_mode_install_to_next_slot),
UNPATCH(R.string.patch_mode_uninstall_patch)
}
var bootSlot by mutableStateOf("")
var bootDev by mutableStateOf("")
var kimgInfo by mutableStateOf(KPModel.KImgInfo("", false))
var kpimgInfo by mutableStateOf(KPModel.KPImgInfo("", "", "", "", ""))
var superkey by mutableStateOf(APApplication.superKey)
var existedExtras = mutableStateListOf<KPModel.IExtraInfo>()
var newExtras = mutableStateListOf<KPModel.IExtraInfo>()
var newExtrasFileName = mutableListOf<String>()
var running by mutableStateOf(false)
var patching by mutableStateOf(false)
var patchdone by mutableStateOf(false)
var needReboot by mutableStateOf(false)
var error by mutableStateOf("")
var patchLog by mutableStateOf("")
private val patchDir: ExtendedFile = FileSystemManager.getLocal().getFile(apApp.filesDir.parent, "patch")
private var srcBoot: ExtendedFile = patchDir.getChildFile("boot.img")
private var shell: Shell = createRootShell()
private var prepared: Boolean = false
private fun prepare() {
patchDir.deleteRecursively()
patchDir.mkdirs()
val execs = listOf(
"libkptools.so", "libmagiskboot.so", "libbusybox.so", "libkpatch.so", "libbootctl.so"
)
error = ""
val info = apApp.applicationInfo
val libs = File(info.nativeLibraryDir).listFiles { _, name ->
execs.contains(name)
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
Os.symlink(lib.path, "$patchDir/$name")
}
// Extract scripts
for (script in listOf(
"boot_patch.sh", "boot_unpatch.sh", "boot_extract.sh", "util_functions.sh", "kpimg"
)) {
val dest = File(patchDir, script)
apApp.assets.open(script).writeTo(dest)
}
}
private fun parseKpimg() {
val result = shellForResult(
shell, "cd $patchDir", "./kptools -l -k kpimg"
)
if (result.isSuccess) {
val ini = Ini(StringReader(result.out.joinToString("\n")))
val kpimg = ini["kpimg"]
if (kpimg != null) {
kpimgInfo = KPModel.KPImgInfo(
kpimg["version"].toString(),
kpimg["compile_time"].toString(),
kpimg["config"].toString(),
APApplication.superKey, // current key
kpimg["root_superkey"].toString(), // empty
)
} else {
error += "parse kpimg error\n"
}
} else {
error = result.err.joinToString("\n")
}
}
private fun parseBootimg(bootimg: String) {
val result = shellForResult(
shell,
"cd $patchDir",
"./magiskboot unpack $bootimg",
"./kptools -l -i kernel",
)
if (result.isSuccess) {
val ini = Ini(StringReader(result.out.joinToString("\n")))
Log.d(TAG, "kernel image info: $ini")
val kernel = ini["kernel"]
if (kernel == null) {
error += "empty kernel section"
Log.d(TAG, error)
return
}
kimgInfo = KPModel.KImgInfo(kernel["banner"].toString(), kernel["patched"].toBoolean())
if (kimgInfo.patched) {
val superkey = ini["kpimg"]?.getOrDefault("superkey", "") ?: ""
kpimgInfo.superKey = superkey
if (checkSuperKeyValidation(superkey)) {
this.superkey = superkey
}
var kpmNum = kernel["extra_num"]?.toInt()
if (kpmNum == null) {
val extras = ini["extras"]
kpmNum = extras?.get("num")?.toInt()
}
if (kpmNum != null && kpmNum > 0) {
for (i in 0..<kpmNum) {
val extra = ini["extra $i"]
if (extra == null) {
error += "empty extra section"
break
}
val type = KPModel.ExtraType.valueOf(extra["type"]!!.uppercase())
val name = extra["name"].toString()
val args = extra["args"].toString()
var event = extra["event"].toString()
if (event.isEmpty()) {
event = KPModel.TriggerEvent.PRE_KERNEL_INIT.event
}
if (type == KPModel.ExtraType.KPM) {
val kpmInfo = KPModel.KPMInfo(
type, name, event, args,
extra["version"].toString(),
extra["license"].toString(),
extra["author"].toString(),
extra["description"].toString(),
)
existedExtras.add(kpmInfo)
}
}
}
}
} else {
error += result.err.joinToString("\n")
}
}
val checkSuperKeyValidation: (superKey: String) -> Boolean = { superKey ->
superKey.length in 8..63 && superKey.any { it.isDigit() } && superKey.any { it.isLetter() }
}
fun copyAndParseBootimg(uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
if (running) return@launch
running = true
try {
uri.inputStream().buffered().use { src ->
srcBoot.also {
src.copyAndCloseOut(it.newOutputStream())
}
}
} catch (e: IOException) {
Log.e(TAG, "copy boot image error: $e")
}
parseBootimg(srcBoot.path)
running = false
}
}
private fun extractAndParseBootimg(mode: PatchMode) {
var cmdBuilder = "./boot_extract.sh"
if (mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
cmdBuilder += " true"
}
val result = shellForResult(
shell,
"export ASH_STANDALONE=1",
"cd $patchDir",
"./busybox sh $cmdBuilder",
)
if (result.isSuccess) {
bootSlot = if (!result.out.toString().contains("SLOT=")) {
""
} else {
result.out.filter { it.startsWith("SLOT=") }[0].removePrefix("SLOT=")
}
bootDev =
result.out.filter { it.startsWith("BOOTIMAGE=") }[0].removePrefix("BOOTIMAGE=")
Log.i(TAG, "current slot: $bootSlot")
Log.i(TAG, "current bootimg: $bootDev")
srcBoot = FileSystemManager.getLocal().getFile(bootDev)
parseBootimg(bootDev)
} else {
error = result.err.joinToString("\n")
}
running = false
}
fun prepare(mode: PatchMode) {
viewModelScope.launch(Dispatchers.IO) {
if (prepared) return@launch
prepared = true
running = true
prepare()
if (mode != PatchMode.UNPATCH) {
parseKpimg()
}
if (mode == PatchMode.PATCH_AND_INSTALL || mode == PatchMode.UNPATCH || mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
extractAndParseBootimg(mode)
}
running = false
}
}
fun embedKPM(uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
if (running) return@launch
running = true
error = ""
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
val kpmFileName = "${rand}.kpm"
val kpmFile: ExtendedFile = patchDir.getChildFile(kpmFileName)
Log.i(TAG, "copy kpm to: " + kpmFile.path)
try {
uri.inputStream().buffered().use { src ->
kpmFile.also {
src.copyAndCloseOut(it.newOutputStream())
}
}
} catch (e: IOException) {
Log.e(TAG, "Copy kpm error: $e")
}
val result = shellForResult(
shell, "cd $patchDir", "./kptools -l -M ${kpmFile.path}"
)
if (result.isSuccess) {
val ini = Ini(StringReader(result.out.joinToString("\n")))
val kpm = ini["kpm"]
if (kpm != null) {
val kpmInfo = KPModel.KPMInfo(
KPModel.ExtraType.KPM,
kpm["name"].toString(),
KPModel.TriggerEvent.PRE_KERNEL_INIT.event,
"",
kpm["version"].toString(),
kpm["license"].toString(),
kpm["author"].toString(),
kpm["description"].toString(),
)
newExtras.add(kpmInfo)
newExtrasFileName.add(kpmFileName)
}
} else {
error = "Invalid KPM\n"
}
running = false
}
}
fun doUnpatch() {
viewModelScope.launch(Dispatchers.IO) {
patching = true
patchLog = ""
Log.i(TAG, "starting unpatching...")
val logs = object : CallbackList<String>() {
override fun onAddElement(e: String?) {
patchLog += e
Log.i(TAG, "" + e)
patchLog += "\n"
}
}
val result = shell.newJob().add(
"export ASH_STANDALONE=1",
"cd $patchDir",
"cp /data/adb/ap/ori.img new-boot.img",
"./busybox sh ./boot_unpatch.sh $bootDev",
"rm -f ${APApplication.APD_PATH}",
"rm -rf ${APApplication.APATCH_FOLDER}",
).to(logs, logs).exec()
if (result.isSuccess) {
logs.add(" Unpatch successful")
needReboot = true
APApplication.markNeedReboot()
} else {
logs.add(" Unpatched failed")
error = result.err.joinToString("\n")
}
logs.add("****************************")
patchdone = true
patching = false
}
}
fun isSuExecutable(): Boolean {
val suFile = File("/system/bin/su")
return suFile.exists() && suFile.canExecute()
}
fun doPatch(mode: PatchMode) {
viewModelScope.launch(Dispatchers.IO) {
patching = true
Log.d(TAG, "starting patching...")
val apVer = Version.getManagerVersion().second
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
val outFilename = "apatch_patched_${apVer}_${BuildConfig.buildKPV}_${rand}.img"
val logs = object : CallbackList<String>() {
override fun onAddElement(e: String?) {
patchLog += e
Log.d(TAG, "" + e)
patchLog += "\n"
}
}
logs.add("****************************")
var patchCommand = mutableListOf("./busybox sh boot_patch.sh \"$0\" \"$@\"")
// adapt for 0.10.7 and lower KP
var isKpOld = false
if (mode == PatchMode.PATCH_AND_INSTALL || mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
val KPCheck = shell.newJob().add("truncate $superkey -Z u:r:magisk:s0 -c whoami").exec()
if (KPCheck.isSuccess && !isSuExecutable()) {
patchCommand.addAll(0, listOf("truncate", APApplication.superKey, "-Z", APApplication.MAGISK_SCONTEXT, "-c"))
patchCommand.addAll(listOf(superkey, srcBoot.path, "true"))
} else {
patchCommand = mutableListOf("./busybox", "sh", "boot_patch.sh")
patchCommand.addAll(listOf(superkey, srcBoot.path, "true"))
isKpOld = true
}
} else {
patchCommand.addAll(0, listOf("sh", "-c"))
patchCommand.addAll(listOf(superkey, srcBoot.path))
}
for (i in 0..<newExtrasFileName.size) {
patchCommand.addAll(listOf("-M", newExtrasFileName[i]))
val extra = newExtras[i]
if (extra.args.isNotEmpty()) {
patchCommand.addAll(listOf("-A", extra.args))
}
if (extra.event.isNotEmpty()) {
patchCommand.addAll(listOf("-V", extra.event))
}
patchCommand.addAll(listOf("-T", extra.type.desc))
}
for (i in 0..<existedExtras.size) {
val extra = existedExtras[i]
patchCommand.addAll(listOf("-E", extra.name))
if (extra.args.isNotEmpty()) {
patchCommand.addAll(listOf("-A", extra.args))
}
if (extra.event.isNotEmpty()) {
patchCommand.addAll(listOf("-V", extra.event))
}
patchCommand.addAll(listOf("-T", extra.type.desc))
}
val builder = ProcessBuilder(patchCommand)
Log.i(TAG, "patchCommand: $patchCommand")
var succ = false
if (isKpOld) {
val resultString = "\"" + patchCommand.joinToString(separator = "\" \"") + "\""
val result = shell.newJob().add(
"export ASH_STANDALONE=1",
"cd $patchDir",
resultString,
).to(logs, logs).exec()
succ = result.isSuccess
} else {
builder.environment().put("ASH_STANDALONE", "1")
builder.directory(patchDir)
builder.redirectErrorStream(true)
val process = builder.start()
Thread {
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
patchLog += line
Log.i(TAG, "" + line)
patchLog += "\n"
}
}
}.start()
succ = process.waitFor() == 0
}
if (!succ) {
val msg = " Patch failed."
error = msg
// error += result.err.joinToString("\n")
logs.add(error)
logs.add("****************************")
patching = false
return@launch
}
if (mode == PatchMode.PATCH_AND_INSTALL) {
logs.add("- Reboot to finish the installation...")
needReboot = true
APApplication.markNeedReboot()
} else if (mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
logs.add("- Connecting boot hal...")
val bootctlStatus = shell.newJob().add(
"cd $patchDir", "chmod 0777 $patchDir/bootctl", "./bootctl hal-info"
).to(logs, logs).exec()
if (!bootctlStatus.isSuccess) {
logs.add("[X] Failed to connect to boot hal, you may need switch slot manually")
} else {
val currSlot = shellForResult(
shell, "cd $patchDir", "./bootctl get-current-slot"
).out.toString()
val targetSlot = if (currSlot.contains("0")) {
1
} else {
0
}
logs.add("- Switching to next slot: $targetSlot...")
val setNextActiveSlot = shell.newJob().add(
"cd $patchDir", "./bootctl set-active-boot-slot $targetSlot"
).exec()
if (setNextActiveSlot.isSuccess) {
logs.add("- Switch done")
logs.add("- Writing boot marker script...")
val markBootableScript = shell.newJob().add(
"mkdir -p /data/adb/post-fs-data.d && rm -rf /data/adb/post-fs-data.d/post_ota.sh && touch /data/adb/post-fs-data.d/post_ota.sh",
"echo \"chmod 0777 $patchDir/bootctl\" > /data/adb/post-fs-data.d/post_ota.sh",
"echo \"chown root:root 0777 $patchDir/bootctl\" > /data/adb/post-fs-data.d/post_ota.sh",
"echo \"$patchDir/bootctl mark-boot-successful\" > /data/adb/post-fs-data.d/post_ota.sh",
"echo >> /data/adb/post-fs-data.d/post_ota.sh",
"echo \"rm -rf $patchDir\" >> /data/adb/post-fs-data.d/post_ota.sh",
"echo >> /data/adb/post-fs-data.d/post_ota.sh",
"echo \"rm -f /data/adb/post-fs-data.d/post_ota.sh\" >> /data/adb/post-fs-data.d/post_ota.sh",
"chmod 0777 /data/adb/post-fs-data.d/post_ota.sh",
"chown root:root /data/adb/post-fs-data.d/post_ota.sh",
).to(logs, logs).exec()
if (markBootableScript.isSuccess) {
logs.add("- Boot marker script write done")
} else {
logs.add("[X] Boot marker scripts write failed")
}
}
}
logs.add("- Reboot to finish the installation...")
needReboot = true
APApplication.markNeedReboot()
} else if (mode == PatchMode.PATCH_ONLY) {
val newBootFile = patchDir.getChildFile("new-boot.img")
val outDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
if (!outDir.exists()) outDir.mkdirs()
val outPath = File(outDir, outFilename)
val inputUri = newBootFile.getUri(apApp)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val outUri = createDownloadUri(apApp, outFilename)
succ = insertDownload(apApp, outUri, inputUri)
} else {
newBootFile.inputStream().copyAndClose(outPath.outputStream())
}
if (succ) {
logs.add(" Output file is written to ")
logs.add(" ${outPath.path}")
} else {
logs.add(" Write patched boot.img failed")
}
}
logs.add("****************************")
patchdone = true
patching = false
}
}
@RequiresApi(Build.VERSION_CODES.Q)
fun createDownloadUri(context: Context, outFilename: String): Uri? {
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, outFilename)
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
return resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
}
@RequiresApi(Build.VERSION_CODES.Q)
fun insertDownload(context: Context, outUri: Uri?, inputUri: Uri): Boolean {
if (outUri == null) return false
try {
val resolver = context.contentResolver
resolver.openInputStream(inputUri)?.use { inputStream ->
resolver.openOutputStream(outUri)?.use { outputStream ->
inputStream.copyTo(outputStream)
}
}
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.IS_PENDING, 0)
}
resolver.update(outUri, contentValues, null, null)
return true
} catch (_: FileNotFoundException) {
return false
}
}
fun File.getUri(context: Context): Uri {
val authority = "${context.packageName}.fileprovider"
return FileProvider.getUriForFile(context, authority, this)
}
}

View file

@ -0,0 +1,153 @@
package me.bmax.apatch.ui.viewmodel
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.Parcelable
import android.util.Log
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.bmax.apatch.IAPRootService
import me.bmax.apatch.Natives
import me.bmax.apatch.apApp
import me.bmax.apatch.services.RootServices
import me.bmax.apatch.util.APatchCli
import me.bmax.apatch.util.HanziToPinyin
import me.bmax.apatch.util.PkgConfig
import java.text.Collator
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class SuperUserViewModel : ViewModel() {
companion object {
private const val TAG = "SuperUserViewModel"
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
}
@Parcelize
data class AppInfo(
val label: String, val packageInfo: PackageInfo, val config: PkgConfig.Config
) : Parcelable {
val packageName: String
get() = packageInfo.packageName
val uid: Int
get() = packageInfo.applicationInfo!!.uid
}
var search by mutableStateOf("")
var showSystemApps by mutableStateOf(false)
var isRefreshing by mutableStateOf(false)
private set
private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> {
when {
it.config.allow != 0 -> 0
it.config.exclude == 1 -> 1
else -> 2
}
}.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
apps.sortedWith(comparator).also {
isRefreshing = false
}
}
val appList by derivedStateOf {
sortedList.filter {
it.label.lowercase().contains(search.lowercase()) || it.packageName.lowercase()
.contains(search.lowercase()) || HanziToPinyin.getInstance()
.toPinyinString(it.label).contains(search.lowercase())
}.filter {
it.uid == 2000 // Always show shell
|| showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
}
}
private suspend inline fun connectRootService(
crossinline onDisconnect: () -> Unit = {}
): Pair<IBinder, ServiceConnection> = suspendCoroutine {
val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect()
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
it.resume(binder as IBinder to this)
}
}
val intent = Intent(apApp, RootServices::class.java)
val task = RootServices.bindOrTask(
intent,
Shell.EXECUTOR,
connection,
)
val shell = APatchCli.SHELL
task?.let { it1 -> shell.execTask(it1) }
}
private fun stopRootService() {
val intent = Intent(apApp, RootServices::class.java)
RootServices.stop(intent)
}
suspend fun fetchAppList() {
isRefreshing = true
val result = connectRootService {
Log.w(TAG, "RootService disconnected")
}
withContext(Dispatchers.IO) {
val binder = result.first
val allPackages = IAPRootService.Stub.asInterface(binder).getPackages(0)
withContext(Dispatchers.Main) {
stopRootService()
}
val uids = Natives.suUids().toList()
Log.d(TAG, "all allows: $uids")
var configs: HashMap<Int, PkgConfig.Config> = HashMap()
thread {
Natives.su()
configs = PkgConfig.readConfigs()
}.join()
Log.d(TAG, "all configs: $configs")
apps = allPackages.list.map {
val appInfo = it.applicationInfo
val uid = appInfo!!.uid
val actProfile = if (uids.contains(uid)) Natives.suProfile(uid) else null
val config = configs.getOrDefault(
uid, PkgConfig.Config(appInfo.packageName, Natives.isUidExcluded(uid), 0, Natives.Profile(uid = uid))
)
config.allow = 0
// from kernel
if (actProfile != null) {
config.allow = 1
config.profile = actProfile
}
AppInfo(
label = appInfo.loadLabel(apApp.packageManager).toString(),
packageInfo = it,
config = config
)
}.filter { it.packageName != apApp.packageName }
}
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.bmax.apatch.ui.webui;
import java.net.URLConnection;
class MimeUtil {
public static String getMimeFromFileName(String fileName) {
if (fileName == null) {
return null;
}
// Copying the logic and mapping that Chromium follows.
// First we check against the OS (this is a limited list by default)
// but app developers can extend this.
// We then check against a list of hardcoded mime types above if the
// OS didn't provide a result.
String mimeType = URLConnection.guessContentTypeFromName(fileName);
if (mimeType != null) {
return mimeType;
}
return guessHardcodedMime(fileName);
}
// We should keep this map in sync with the lists under
// //net/base/mime_util.cc in Chromium.
// A bunch of the mime types don't really apply to Android land
// like word docs so feel free to filter out where necessary.
private static String guessHardcodedMime(String fileName) {
int finalFullStop = fileName.lastIndexOf('.');
if (finalFullStop == -1) {
return null;
}
final String extension = fileName.substring(finalFullStop + 1).toLowerCase();
switch (extension) {
case "webm":
return "video/webm";
case "mpeg":
case "mpg":
return "video/mpeg";
case "mp3":
return "audio/mpeg";
case "wasm":
return "application/wasm";
case "xhtml":
case "xht":
case "xhtm":
return "application/xhtml+xml";
case "flac":
return "audio/flac";
case "ogg":
case "oga":
case "opus":
return "audio/ogg";
case "wav":
return "audio/wav";
case "m4a":
return "audio/x-m4a";
case "gif":
return "image/gif";
case "jpeg":
case "jpg":
case "jfif":
case "pjpeg":
case "pjp":
return "image/jpeg";
case "png":
return "image/png";
case "apng":
return "image/apng";
case "svg":
case "svgz":
return "image/svg+xml";
case "webp":
return "image/webp";
case "mht":
case "mhtml":
return "multipart/related";
case "css":
return "text/css";
case "html":
case "htm":
case "shtml":
case "shtm":
case "ehtml":
return "text/html";
case "js":
case "mjs":
return "application/javascript";
case "xml":
return "text/xml";
case "mp4":
case "m4v":
return "video/mp4";
case "ogv":
case "ogm":
return "video/ogg";
case "ico":
return "image/x-icon";
case "woff":
return "application/font-woff";
case "gz":
case "tgz":
return "application/gzip";
case "json":
return "application/json";
case "pdf":
return "application/pdf";
case "zip":
return "application/zip";
case "bmp":
return "image/bmp";
case "tiff":
case "tif":
return "image/tiff";
default:
return null;
}
}
}

View file

@ -0,0 +1,195 @@
package me.bmax.apatch.ui.webui;
import android.content.Context;
import android.util.Log;
import android.webkit.WebResourceResponse;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.webkit.WebViewAssetLoader;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
import me.bmax.apatch.util.APatchCliKt;
/**
* Handler class to open files from file system by root access
* For more information about android storage please refer to
* <a href="https://developer.android.com/guide/topics/data/data-storage">Android Developers
* Docs: Data and file storage overview</a>.
* <p class="note">
* To avoid leaking user or app data to the web, make sure to choose {@code directory}
* carefully, and assume any file under this directory could be accessed by any web page subject
* to same-origin rules.
* <p>
* A typical usage would be like:
* <pre class="prettyprint">
* File publicDir = new File(context.getFilesDir(), "public");
* // Host "files/public/" in app's data directory under:
* // http://appassets.androidplatform.net/public/...
* WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
* .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
* .build();
* </pre>
*/
public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
private static final String TAG = "SuFilePathHandler";
/**
* Default value to be used as MIME type if guessing MIME type failed.
*/
public static final String DEFAULT_MIME_TYPE = "text/plain";
/**
* Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this
* handler. They are forbidden as they often contain sensitive information.
* <p class="note">
* Note: Any future addition to this list will be considered breaking changes to the API.
*/
private static final String[] FORBIDDEN_DATA_DIRS =
new String[] {"/data/data", "/data/system"};
@NonNull
private final File mDirectory;
private final Shell mShell;
/**
* Creates PathHandler for app's internal storage.
* The directory to be exposed must be inside either the application's internal data
* directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}.
* External storage is not supported for security reasons, as other apps with
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the
* files.
* <p>
* Exposing the entire data or cache directory is not permitted, to avoid accidentally
* exposing sensitive application files to the web. Certain existing subdirectories of
* {@link Context#getDataDir} are also not permitted as they are often sensitive.
* These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"},
* {@code "shared_prefs/"} and {@code "code_cache/"}).
* <p>
* The application should typically use a dedicated subdirectory for the files it intends to
* expose and keep them separate from other files.
*
* @param context {@link Context} that is used to access app's internal storage.
* @param directory the absolute path of the exposed app internal storage directory from
* which files can be loaded.
* @throws IllegalArgumentException if the directory is not allowed.
*/
public SuFilePathHandler(@NonNull Context context, @NonNull File directory) {
try {
mDirectory = new File(getCanonicalDirPath(directory));
if (!isAllowedInternalStorageDir(context)) {
throw new IllegalArgumentException("The given directory \"" + directory
+ "\" doesn't exist under an allowed app internal storage directory");
}
mShell = APatchCliKt.createRootShell(true);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve the canonical path for the given directory: "
+ directory.getPath(), e);
}
}
private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException {
String dir = getCanonicalDirPath(mDirectory);
for (String forbiddenPath : FORBIDDEN_DATA_DIRS) {
if (dir.startsWith(forbiddenPath)) {
return false;
}
}
return true;
}
/**
* Opens the requested file from the exposed data directory.
* <p>
* The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the
* requested file cannot be found or is outside the mounted directory a
* {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be
* returned instead of {@code null}. This saves the time of falling back to network and
* trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with
* {@code null} {@link InputStream} will be received as an HTTP response with status code
* {@code 404} and no body.
* <p class="note">
* The MIME type for the file will be determined from the file's extension using
* {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that
* files are named using standard file extensions. If the file does not have a
* recognised extension, {@code "text/plain"} will be used by default.
*
* @param path the suffix path to be handled.
* @return {@link WebResourceResponse} for the requested file.
*/
@Override
@WorkerThread
@NonNull
public WebResourceResponse handle(@NonNull String path) {
try {
File file = getCanonicalFileIfChild(mDirectory, path);
if (file != null) {
InputStream is = openFile(file, mShell);
String mimeType = guessMimeType(path);
return new WebResourceResponse(mimeType, null, is);
} else {
Log.e(TAG, String.format(
"The requested file: %s is outside the mounted directory: %s", path,
mDirectory));
}
} catch (IOException e) {
Log.e(TAG, "Error opening the requested path: " + path, e);
}
return new WebResourceResponse(null, null, null);
}
public static String getCanonicalDirPath(@NonNull File file) throws IOException {
String canonicalPath = file.getCanonicalPath();
if (!canonicalPath.endsWith("/")) canonicalPath += "/";
return canonicalPath;
}
public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child)
throws IOException {
String parentCanonicalPath = getCanonicalDirPath(parent);
String childCanonicalPath = new File(parent, child).getCanonicalPath();
if (childCanonicalPath.startsWith(parentCanonicalPath)) {
return new File(childCanonicalPath);
}
return null;
}
@NonNull
private static InputStream handleSvgzStream(@NonNull String path,
@NonNull InputStream stream) throws IOException {
return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
}
public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws FileNotFoundException,
IOException {
SuFile suFile = new SuFile(file.getAbsolutePath());
suFile.setShell(shell);
InputStream fis = SuFileInputStream.open(suFile);
return handleSvgzStream(file.getPath(), fis);
}
/**
* Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the
* {@link #DEFAULT_MIME_TYPE} if it can't guess.
*
* @param filePath path of the file to guess its MIME type.
* @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}.
*/
@NonNull
public static String guessMimeType(@NonNull String filePath) {
String mimeType = MimeUtil.getMimeFromFileName(filePath);
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
}
}

View file

@ -0,0 +1,175 @@
package me.bmax.apatch.ui.webui
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.view.Window
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.Toast
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import me.bmax.apatch.util.createRootShell
import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.CompletableFuture
class WebViewInterface(val context: Context, private val webView: WebView) {
@JavascriptInterface
fun exec(cmd: String): String {
val shell = createRootShell()
return ShellUtils.fastCmd(shell, cmd)
}
@JavascriptInterface
fun exec(cmd: String, callbackFunc: String) {
exec(cmd, null, callbackFunc)
}
private fun processOptions(sb: StringBuilder, options: String?) {
val opts = if (options == null) JSONObject() else {
JSONObject(options)
}
val cwd = opts.optString("cwd")
if (!TextUtils.isEmpty(cwd)) {
sb.append("cd ${cwd};")
}
opts.optJSONObject("env")?.let { env ->
env.keys().forEach { key ->
sb.append("export ${key}=${env.getString(key)};")
}
}
}
@JavascriptInterface
fun exec(
cmd: String, options: String?, callbackFunc: String
) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
finalCommand.append(cmd)
val shell = createRootShell()
val result = shell.newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec()
val stdout = result.out.joinToString(separator = "\n")
val stderr = result.err.joinToString(separator = "\n")
val jsCode = "javascript: (function() { try { ${callbackFunc}(${result.code}, ${
JSONObject.quote(
stdout
)
}, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
@JavascriptInterface
fun spawn(command: String, args: String, options: String?, callbackFunc: String) {
val finalCommand = StringBuilder()
processOptions(finalCommand, options)
if (!TextUtils.isEmpty(args)) {
finalCommand.append(command).append(" ")
JSONArray(args).let { argsArray ->
for (i in 0 until argsArray.length()) {
finalCommand.append(argsArray.getString(i))
finalCommand.append(" ")
}
}
} else {
finalCommand.append(command)
}
val shell = createRootShell()
val emitData = fun(name: String, data: String) {
val jsCode = "javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${
JSONObject.quote(
data
)
}); } catch(e) { console.error('emitData', e); } })();"
webView.post {
webView.loadUrl(jsCode)
}
}
val stdout = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stdout", s)
}
}
val stderr = object : CallbackList<String>(UiThreadHandler::runAndWait) {
override fun onAddElement(s: String) {
emitData("stderr", s)
}
}
val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()
val completableFuture = CompletableFuture.supplyAsync {
future.get()
}
completableFuture.thenAccept { result ->
val emitExitCode =
"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
webView.post {
webView.loadUrl(emitExitCode)
}
if (result.code != 0) {
val emitErrCode =
"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${
JSONObject.quote(
result.err.joinToString(
"\n"
)
)
};${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();"
webView.post {
webView.loadUrl(emitErrCode)
}
}
}
}
@JavascriptInterface
fun toast(msg: String) {
webView.post {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun fullScreen(enable: Boolean) {
if (context is Activity) {
Handler(Looper.getMainLooper()).post {
if (enable) {
hideSystemUI(context.window)
} else {
showSystemUI(context.window)
}
}
}
}
}
fun hideSystemUI(window: Window) {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())

View file

@ -0,0 +1,391 @@
package me.bmax.apatch.util
import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import android.util.Base64
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 me.bmax.apatch.APApplication
import me.bmax.apatch.APApplication.Companion.SUPERCMD
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.apApp
import me.bmax.apatch.ui.screen.MODULE_TYPE
import java.io.File
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.zip.ZipFile
private const val TAG = "APatchCli"
private fun getKPatchPath(): String {
return apApp.applicationInfo.nativeLibraryDir + File.separator + "libkpatch.so"
}
class RootShellInitializer : Shell.Initializer() {
override fun onInit(context: Context, shell: Shell): Boolean {
shell.newJob().add("export PATH=\$PATH:/system_ext/bin:/vendor/bin").exec()
return true
}
}
fun createRootShell(globalMnt: Boolean = false): Shell {
Shell.enableVerboseLogging = BuildConfig.DEBUG
val builder = Shell.Builder.create().setInitializers(RootShellInitializer::class.java)
return try {
builder.build(
SUPERCMD, APApplication.superKey, "-Z", APApplication.MAGISK_SCONTEXT
)
} catch (e: Throwable) {
Log.e(TAG, "su failed: ", e)
return try {
Log.e(TAG, "retry compat kpatch su")
if (globalMnt) {
builder.build(
getKPatchPath(), APApplication.superKey, "su", "-Z", APApplication.MAGISK_SCONTEXT, "--mount-master"
)
}else{
builder.build(
getKPatchPath(), APApplication.superKey, "su", "-Z", APApplication.MAGISK_SCONTEXT
)
}
} catch (e: Throwable) {
Log.e(TAG, "retry kpatch su failed: ", e)
return try {
Log.e(TAG, "retry su: ", e)
if (globalMnt) {
builder.build("su","-mm")
}else{
builder.build("su")
}
} catch (e: Throwable) {
Log.e(TAG, "retry su failed: ", e)
return builder.build("sh")
}
}
}
}
object APatchCli {
var SHELL: Shell = createRootShell()
val GLOBAL_MNT_SHELL: Shell = createRootShell(true)
fun refresh() {
val tmp = SHELL
SHELL = createRootShell()
tmp.close()
}
}
fun getRootShell(globalMnt: Boolean = false): Shell {
return if (globalMnt) APatchCli.GLOBAL_MNT_SHELL else {
APatchCli.SHELL
}
}
inline fun <T> withNewRootShell(
globalMnt: Boolean = false,
block: Shell.() -> T
): T {
return createRootShell(globalMnt).use(block)
}
fun rootAvailable(): Boolean {
val shell = getRootShell()
return shell.isRoot
}
fun tryGetRootShell(): Shell {
Shell.enableVerboseLogging = BuildConfig.DEBUG
val builder = Shell.Builder.create()
return try {
builder.build(
SUPERCMD, APApplication.superKey, "-Z", APApplication.MAGISK_SCONTEXT
)
} catch (e: Throwable) {
Log.e(TAG, "su failed: ", e)
return try {
Log.e(TAG, "retry compat kpatch su")
builder.build(
getKPatchPath(), APApplication.superKey, "su", "-Z", APApplication.MAGISK_SCONTEXT
)
} catch (e: Throwable) {
Log.e(TAG, "retry kpatch su failed: ", e)
return try {
Log.e(TAG, "retry su: ", e)
builder.build("su")
} catch (e: Throwable) {
Log.e(TAG, "retry su failed: ", e)
builder.build("sh")
}
}
}
}
fun shellForResult(shell: Shell, vararg cmds: String): Shell.Result {
val out = ArrayList<String>()
val err = ArrayList<String>()
return shell.newJob().add(*cmds).to(out, err).exec()
}
fun rootShellForResult(vararg cmds: String): Shell.Result {
val out = ArrayList<String>()
val err = ArrayList<String>()
return getRootShell().newJob().add(*cmds).to(out, err).exec()
}
fun execApd(args: String, newShell: Boolean = false): Boolean {
return if (newShell) {
withNewRootShell {
ShellUtils.fastCmdResult(this, "${APApplication.APD_PATH} $args")
}
} else {
ShellUtils.fastCmdResult(getRootShell(), "${APApplication.APD_PATH} $args")
}
}
fun listModules(): String {
val shell = getRootShell()
val out =
shell.newJob().add("${APApplication.APD_PATH} module list").to(ArrayList(), null).exec().out
withNewRootShell{
newJob().add("cp /data/user/*/me.bmax.apatch/patch/ori.img /data/adb/ap/ && rm /data/user/*/me.bmax.apatch/patch/ori.img")
.to(ArrayList(),null).exec()
}
return out.joinToString("\n").ifBlank { "[]" }
}
fun toggleModule(id: String, enable: Boolean): Boolean {
val cmd = if (enable) {
"module enable $id"
} else {
"module disable $id"
}
val result = execApd(cmd,true)
Log.i(TAG, "$cmd result: $result")
return result
}
fun uninstallModule(id: String): Boolean {
val cmd = "module uninstall $id"
val result = execApd(cmd,true)
Log.i(TAG, "uninstall module $id result: $result")
return result
}
fun installModule(
uri: Uri, type: MODULE_TYPE, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean {
val resolver = apApp.contentResolver
with(resolver.openInputStream(uri)) {
val file = File(apApp.cacheDir, "module_$type.zip")
file.outputStream().use { output ->
this?.copyTo(output)
}
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStdout(s ?: "")
}
}
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStderr(s ?: "")
}
}
val shell = getRootShell()
var result = false
if(type == MODULE_TYPE.APM) {
val cmd = "${APApplication.APD_PATH} module install ${file.absolutePath}"
result = shell.newJob().add(cmd).to(stdoutCallback, stderrCallback)
.exec().isSuccess
} else {
// ZipUtils.
}
Log.i(TAG, "install $type module $uri result: $result")
file.delete()
onFinish(result)
return result
}
}
fun runAPModuleAction(
moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean {
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStdout(s ?: "")
}
}
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStderr(s ?: "")
}
}
val result = withNewRootShell{
newJob().add("${APApplication.APD_PATH} module action $moduleId")
.to(stdoutCallback, stderrCallback).exec()
}
Log.i(TAG, "APModule runAction result: $result")
return result.isSuccess
}
fun reboot(reason: String = "") {
if (reason == "recovery") {
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
getRootShell().newJob().add("/system/bin/input keyevent 26").exec()
}
getRootShell().newJob()
.add("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").exec()
}
fun overlayFsAvailable(): Boolean {
val shell = getRootShell()
return ShellUtils.fastCmdResult(shell, "grep overlay /proc/filesystems")
}
fun hasMagisk(): Boolean {
val shell = getRootShell()
val result = shell.newJob().add("nsenter --mount=/proc/1/ns/mnt which magisk").exec()
Log.i(TAG, "has magisk: ${result.isSuccess}")
return result.isSuccess
}
fun isGlobalNamespaceEnabled(): Boolean {
val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "cat ${APApplication.GLOBAL_NAMESPACE_FILE}")
Log.i(TAG, "is global namespace enabled: $result")
return result == "1"
}
fun setGlobalNamespaceEnabled(value: String) {
getRootShell().newJob().add("echo $value > ${APApplication.GLOBAL_NAMESPACE_FILE}")
.submit { result ->
Log.i(TAG, "setGlobalNamespaceEnabled result: ${result.isSuccess} [${result.out}]")
}
}
fun isLiteModeEnabled(): Boolean {
val liteMode = SuFile(APApplication.LITE_MODE_FILE)
liteMode.shell = getRootShell()
return liteMode.exists()
}
fun setLiteMode(enable: Boolean) {
getRootShell().newJob().add("${if (enable) "touch" else "rm -rf"} ${APApplication.LITE_MODE_FILE}")
.submit { result ->
Log.i(TAG, "setLiteMode result: ${result.isSuccess} [${result.out}]")
}
}
fun isForceUsingOverlayFS(): Boolean {
val forceOverlayFS = SuFile(APApplication.FORCE_OVERLAYFS_FILE)
forceOverlayFS.shell = getRootShell()
return forceOverlayFS.exists()
}
fun setForceUsingOverlayFS(enable: Boolean) {
getRootShell().newJob().add("${if (enable) "touch" else "rm -rf"} ${APApplication.FORCE_OVERLAYFS_FILE}")
.submit { result ->
Log.i(TAG, "setForceUsingOverlayFS result: ${result.isSuccess} [${result.out}]")
}
}
fun getFileNameFromUri(context: Context, uri: Uri): String? {
var fileName: String? = null
val contentResolver: ContentResolver = context.contentResolver
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
}
}
return fileName
}
@Suppress("DEPRECATION")
private fun signatureFromAPI(context: Context): ByteArray? {
return try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
context.packageManager.getPackageInfo(
context.packageName, PackageManager.GET_SIGNING_CERTIFICATES
)
} else {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
}
val signatures: Array<out Signature>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.signingInfo?.apkContentsSigners
} else {
packageInfo.signatures
}
signatures?.firstOrNull()?.toByteArray()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun signatureFromAPK(context: Context): ByteArray? {
var signatureBytes: ByteArray? = null
try {
ZipFile(context.packageResourcePath).use { zipFile ->
val entries = zipFile.entries()
while (entries.hasMoreElements() && signatureBytes == null) {
val entry = entries.nextElement()
if (entry.name.matches("(META-INF/.*)\\.(RSA|DSA|EC)".toRegex())) {
zipFile.getInputStream(entry).use { inputStream ->
val certFactory = CertificateFactory.getInstance("X509")
val x509Cert =
certFactory.generateCertificate(inputStream) as X509Certificate
signatureBytes = x509Cert.encoded
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return signatureBytes
}
private fun validateSignature(signatureBytes: ByteArray?, validSignature: String): Boolean {
signatureBytes ?: return false
val digest = MessageDigest.getInstance("SHA-256")
val signatureHash = Base64.encodeToString(digest.digest(signatureBytes), Base64.NO_WRAP)
return signatureHash == validSignature
}
fun verifyAppSignature(validSignature: String): Boolean {
val context = apApp.applicationContext
val apkSignature = signatureFromAPK(context)
val apiSignature = signatureFromAPI(context)
return validateSignature(apiSignature, validSignature) && validateSignature(
apkSignature,
validSignature
)
}

View file

@ -0,0 +1,149 @@
package me.bmax.apatch.util;
import android.content.SharedPreferences;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class APatchKeyHelper {
protected static final String SUPER_KEY = "super_key";
protected static final String SUPER_KEY_ENC = "super_key_enc";
private static final String TAG = "APatchSecurityHelper";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final String SKIP_STORE_SUPER_KEY = "skip_store_super_key";
private static final String SUPER_KEY_IV = "super_key_iv";
private static final String KEY_ALIAS = "APatchSecurityKey";
private static final String ENCRYPT_MODE = "AES/GCM/NoPadding";
private static SharedPreferences prefs = null;
static {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS)) {
generateSecretKey();
}
} catch (Exception e) {
Log.e(TAG, "Failed to checkAndGenerateSecretKey", e);
}
}
public static void setSharedPreferences(SharedPreferences sp) {
prefs = sp;
}
private static void generateSecretKey() {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_ALIAS)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
AlgorithmParameterSpec spec = new KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(false)
.build();
keyGenerator.init(spec);
keyGenerator.generateKey();
}
} catch (Exception e) {
Log.e(TAG, "Failed to generateSecretKey", e);
}
}
private static String getRandomIV() {
String randIV = prefs.getString(SUPER_KEY_IV, null);
if (randIV == null) {
SecureRandom secureRandom = new SecureRandom();
byte[] generated = secureRandom.generateSeed(12);
randIV = Base64.encodeToString(generated, Base64.DEFAULT);
prefs.edit().putString(SUPER_KEY_IV, randIV).apply();
}
return randIV;
}
private static String encrypt(String orig) {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_ALIAS, null);
Cipher cipher = Cipher.getInstance(ENCRYPT_MODE);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, Base64.decode(getRandomIV(), Base64.DEFAULT)));
return Base64.encodeToString(cipher.doFinal(orig.getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT);
} catch (Exception e) {
Log.e(TAG, "Failed to encrypt: ", e);
return null;
}
}
private static String decrypt(String encryptedData) {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_ALIAS, null);
Cipher cipher = Cipher.getInstance(ENCRYPT_MODE);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, Base64.decode(getRandomIV(), Base64.DEFAULT)));
return new String(cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT)), StandardCharsets.UTF_8);
} catch (Exception e) {
Log.e(TAG, "Failed to decrypt", e);
return null;
}
}
public static boolean shouldSkipStoreSuperKey() {
return prefs.getInt(SKIP_STORE_SUPER_KEY, 0) != 0;
}
public static void clearConfigKey() {
prefs.edit().remove(SUPER_KEY).apply();
prefs.edit().remove(SUPER_KEY_ENC).apply();
prefs.edit().remove(SUPER_KEY_IV).apply();
}
public static void setShouldSkipStoreSuperKey(boolean should) {
clearConfigKey();
prefs.edit().putInt(SKIP_STORE_SUPER_KEY, should ? 1 : 0).apply();
}
public static String readSPSuperKey() {
String encKey = prefs.getString(SUPER_KEY_ENC, "");
if (!encKey.isEmpty()) {
return decrypt(encKey);
}
@Deprecated()
String key = prefs.getString(SUPER_KEY, "");
writeSPSuperKey(key);
prefs.edit().remove(SUPER_KEY).apply();
return key;
}
public static void writeSPSuperKey(String key) {
if (shouldSkipStoreSuperKey()) return;
key = APatchKeyHelper.encrypt(key);
prefs.edit().putString(SUPER_KEY_ENC, key).apply();
}
}

View file

@ -0,0 +1,52 @@
package me.bmax.apatch.util
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.topjohnwu.superuser.Shell
import me.bmax.apatch.R
@Composable
fun getSELinuxStatus(): String {
val shell = Shell.Builder.create()
.build("sh")
val list = ArrayList<String>()
val result = shell.newJob().add("getenforce").to(list, list).exec()
val output = result.out.joinToString("\n").trim()
if (result.isSuccess) {
return when (output) {
"Enforcing" -> stringResource(R.string.home_selinux_status_enforcing)
"Permissive" -> stringResource(R.string.home_selinux_status_permissive)
"Disabled" -> stringResource(R.string.home_selinux_status_disabled)
else -> stringResource(R.string.home_selinux_status_unknown)
}
}
return if (output.endsWith("Permission denied")) {
stringResource(R.string.home_selinux_status_enforcing)
} else {
stringResource(R.string.home_selinux_status_unknown)
}
}
private fun getSystemProperty(key: String): Boolean {
try {
val c = Class.forName("android.os.SystemProperties")
val get = c.getMethod(
"getBoolean",
String::class.java,
Boolean::class.javaPrimitiveType
)
return get.invoke(c, key, false) as Boolean
} catch (e: Exception) {
Log.e("APatch", "[DeviceUtils] Failed to get system property: ", e)
}
return false
}
// Check to see if device supports A/B (seamless) system updates
fun isABDevice(): Boolean {
return getSystemProperty("ro.build.ab_update")
}

View file

@ -0,0 +1,128 @@
package me.bmax.apatch.util
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import me.bmax.apatch.apApp
import androidx.core.content.ContextCompat
@SuppressLint("Range")
fun download(
context: Context,
url: String,
fileName: String,
description: String,
onDownloaded: (Uri) -> Unit = {},
onDownloading: () -> Unit = {}
) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterByStatus(DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING)
downloadManager.query(query).use { cursor ->
while (cursor.moveToNext()) {
val uri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI))
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
val columnTitle = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))
if (url == uri || fileName == columnTitle) {
if (status == DownloadManager.STATUS_RUNNING || status == DownloadManager.STATUS_PENDING) {
onDownloading()
return
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
onDownloaded(Uri.parse(localUri))
return
}
}
}
}
val request = DownloadManager.Request(Uri.parse(url)).setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS, fileName
).setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setMimeType("application/zip").setTitle(fileName).setDescription(description)
downloadManager.enqueue(request)
}
fun checkNewVersion(): LatestVersionInfo {
val url = "https://api.github.com/repos/bmax121/APatch/releases/latest"
val defaultValue = LatestVersionInfo()
runCatching {
apApp.okhttpClient.newCall(okhttp3.Request.Builder().url(url).build()).execute()
.use { response ->
if (!response.isSuccessful) {
return defaultValue
}
val body = response.body?.string() ?: return defaultValue
val json = org.json.JSONObject(body)
val changelog = json.optString("body")
val versionCode = json.getInt("name")
val assets = json.getJSONArray("assets")
for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
val name = asset.getString("name")
if (!name.endsWith(".apk")) {
continue
}
val downloadUrl = asset.getString("browser_download_url")
return LatestVersionInfo(
versionCode, downloadUrl, changelog
)
}
}
}
return defaultValue
}
@Composable
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
DisposableEffect(context) {
val receiver = object : BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
val id = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, -1
)
val query = DownloadManager.Query().setFilterById(id)
val downloadManager =
context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val status = cursor.getInt(
cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
)
if (status == DownloadManager.STATUS_SUCCESSFUL) {
val uri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
)
onDownloaded(Uri.parse(uri))
}
}
}
}
}
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
ContextCompat.registerReceiver(
context,
receiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
onDispose {
context.unregisterReceiver(receiver)
}
}
}

View file

@ -0,0 +1,569 @@
package me.bmax.apatch.util;
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.text.TextUtils;
import android.util.Log;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Locale;
/**
* An object to convert Chinese character to its corresponding pinyin string. For characters with
* multiple possible pinyin string, only one is selected according to collator. Polyphone is not
* supported in this implementation. This class is implemented to achieve the best runtime
* performance and minimum runtime resources with tolerable sacrifice of accuracy. This
* implementation highly depends on zh_CN ICU collation data and must be always synchronized with
* ICU.
* <p>
* Currently this file is aligned to zh.txt in ICU 4.6
*/
public class HanziToPinyin {
/**
* Unihans array.
* <p>
* Each unihans is the first one within same pinyin when collator is zh_CN.
*/
public static final char[] UNIHANS = {
'\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b',
'\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954',
'\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43',
'\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2',
'\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe',
'\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284',
'\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403',
'\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb',
'\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306',
'\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413',
'\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a',
'\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201',
'\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be',
'\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6',
'\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a',
'\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11',
'\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b',
'\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe',
'\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52',
'\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f',
'\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677',
'\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf',
'\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0',
'\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755',
'\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b',
'\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c',
'\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938',
'\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269',
'\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b',
'\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f',
'\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6',
'\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1',
'\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264',
'\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa',
'\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c',
'\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149',
'\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041',
'\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f',
'\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974',
'\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4',
'\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478',
'\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15',
'\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03',
'\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2',
'\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a',
'\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba',
'\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c',
'\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2',
'\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7',
'\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962',
'\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce',
'\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01',
'\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf',
'\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc',
'\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254',
'\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077',
'\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75',
'\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1',
'\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61',
'\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11',
'\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079',
'\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94',
'\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0',
'\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142',
'\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897',
'\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577',
'\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9',
'\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd',
'\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72',
'\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a',
'\u6628', '\u5159', '\u9fc3', '\u9fc4',};
/**
* Pinyin array.
* <p>
* Each pinyin is corresponding to unihans of same
* offset in the unihans array.
*/
public static final byte[][] PINYINS = {
{65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0},
{65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0},
{65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0},
{66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0},
{66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0},
{66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0},
{66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0},
{66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0},
{66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0},
{66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0},
{66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0},
{67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0},
{67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0},
{67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0},
{67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0},
{67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0},
{67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0},
{67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0},
{67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0},
{83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0},
{67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0},
{67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0},
{67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0},
{67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0},
{67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0},
{67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0},
{67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0},
{67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0},
{67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0},
{67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0},
{68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0},
{68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0},
{68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0},
{68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0},
{68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0},
{68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0},
{68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0},
{68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0},
{68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0},
{68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0},
{68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0},
{69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0},
{69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0},
{69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0},
{70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0},
{70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0},
{70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0},
{70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0},
{70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0},
{71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0},
{71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0},
{71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0},
{71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0},
{71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0},
{71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0},
{71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0},
{71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0},
{71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0},
{72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0},
{72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0},
{72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0},
{72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0},
{72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0},
{72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0},
{72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0},
{72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0},
{72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0},
{72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0},
{74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0},
{74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0},
{74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0},
{74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0},
{74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0},
{74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0},
{74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
{75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0},
{75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0},
{75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0},
{75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0},
{75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0},
{75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0},
{75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0},
{75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0},
{75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0},
{76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0},
{76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0},
{76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0},
{76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0},
{76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0},
{76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0},
{76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0},
{76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0},
{76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0},
{76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0},
{76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0},
{76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0},
{76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0},
{77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0},
{77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0},
{77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0},
{77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0},
{77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0},
{77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0},
{77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0},
{77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0},
{77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0},
{77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0},
{78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0},
{78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0},
{78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0},
{78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0},
{78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0},
{78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0},
{78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0},
{78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0},
{78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0},
{78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0},
{78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0},
{78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0},
{79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0},
{80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0},
{80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0},
{80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0},
{80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0},
{80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0},
{80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0},
{80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0},
{80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0},
{80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0},
{81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0},
{81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0},
{81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0},
{81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0},
{81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0},
{81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0},
{81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0},
{82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0},
{82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0},
{82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0},
{82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0},
{82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0},
{82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0},
{82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0},
{83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0},
{83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0},
{83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0},
{83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0},
{83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0},
{83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0},
{83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0},
{83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0},
{83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0},
{83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0},
{83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0},
{83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0},
{83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0},
{83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0},
{83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0},
{83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0},
{83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0},
{83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0},
{84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0},
{84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0},
{84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0},
{84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0},
{84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0},
{84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0},
{84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0},
{84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0},
{84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0},
{84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0},
{87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0},
{87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0},
{87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0},
{87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0},
{88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0},
{88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0},
{88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0},
{88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0},
{88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0},
{88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0},
{88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0},
{89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0},
{89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0},
{89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0},
{89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0},
{89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0},
{89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0},
{89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0},
{89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
{89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0},
{90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0},
{90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0},
{90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0},
{90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0},
{90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0},
{90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0},
{67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0},
{90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0},
{90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0},
{90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0},
{90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0},
{90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0},
{90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0},
{90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71},
{90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0},
{90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0},
{90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0},
{90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0},
{90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0},
{90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0},
{83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},};
private static final String TAG = "HanziToPinyin";
// Turn on this flag when we want to check internal data structure.
private static final boolean DEBUG = false;
/**
* First and last Chinese character with known Pinyin according to zh collation
*/
private static final String FIRST_PINYIN_UNIHAN = "\u963F";
private static final String LAST_PINYIN_UNIHAN = "\u9FFF";
private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);
private static HanziToPinyin sInstance;
private final boolean mHasChinaCollator;
protected HanziToPinyin(boolean hasChinaCollator) {
mHasChinaCollator = hasChinaCollator;
}
public static HanziToPinyin getInstance() {
synchronized (HanziToPinyin.class) {
if (sInstance != null) {
return sInstance;
}
// Check if zh_CN collation data is available
final Locale[] locale = Collator.getAvailableLocales();
for (int i = 0; i < locale.length; i++) {
if (locale[i].equals(Locale.CHINA) || locale[i].getLanguage().contains("zh")) {
// Do self validation just once.
if (DEBUG) {
Log.d(TAG, "Self validation. Result: " + doSelfValidation());
}
sInstance = new HanziToPinyin(true);
return sInstance;
}
}
if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题
if (Locale.CHINA.equals(Locale.getDefault())) {
sInstance = new HanziToPinyin(true);
return sInstance;
}
}
Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
sInstance = new HanziToPinyin(false);
return sInstance;
}
}
/**
* Validate if our internal table has some wrong value.
*
* @return true when the table looks correct.
*/
private static boolean doSelfValidation() {
char lastChar = UNIHANS[0];
String lastString = Character.toString(lastChar);
for (char c : UNIHANS) {
if (lastChar == c) {
continue;
}
final String curString = Character.toString(c);
int cmp = COLLATOR.compare(lastString, curString);
if (cmp >= 0) {
Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString
+ "\" is greater than current string \"" + curString + "\".");
return false;
}
lastString = curString;
}
return true;
}
private Token getToken(char character) {
Token token = new Token();
final String letter = Character.toString(character);
token.source = letter;
int offset = -1;
int cmp;
if (character < 256) {
token.type = Token.LATIN;
token.target = letter;
return token;
} else {
cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN);
if (cmp < 0) {
token.type = Token.UNKNOWN;
token.target = letter;
return token;
} else if (cmp == 0) {
token.type = Token.PINYIN;
offset = 0;
} else {
cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN);
if (cmp > 0) {
token.type = Token.UNKNOWN;
token.target = letter;
return token;
} else if (cmp == 0) {
token.type = Token.PINYIN;
offset = UNIHANS.length - 1;
}
}
}
token.type = Token.PINYIN;
if (offset < 0) {
int begin = 0;
int end = UNIHANS.length - 1;
while (begin <= end) {
offset = (begin + end) / 2;
final String unihan = Character.toString(UNIHANS[offset]);
cmp = COLLATOR.compare(letter, unihan);
if (cmp == 0) {
break;
} else if (cmp > 0) {
begin = offset + 1;
} else {
end = offset - 1;
}
}
}
if (cmp < 0) {
offset--;
}
StringBuilder pinyin = new StringBuilder();
for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) {
pinyin.append((char) PINYINS[offset][j]);
}
token.target = pinyin.toString();
if (TextUtils.isEmpty(token.target)) {
token.type = Token.UNKNOWN;
token.target = token.source;
}
return token;
}
/**
* Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without
* space will be put into a Token, One Hanzi character which has pinyin will be treated as a
* Token. If these is no China collator, the empty token array is returned.
*/
public ArrayList<Token> get(final String input) {
ArrayList<Token> tokens = new ArrayList<Token>();
if (!mHasChinaCollator || TextUtils.isEmpty(input)) {
// return empty tokens.
return tokens;
}
final int inputLength = input.length();
final StringBuilder sb = new StringBuilder();
int tokenType = Token.LATIN;
// Go through the input, create a new token when
// a. Token type changed
// b. Get the Pinyin of current charater.
// c. current character is space.
for (int i = 0; i < inputLength; i++) {
final char character = input.charAt(i);
if (character == ' ') {
if (sb.length() > 0) {
addToken(sb, tokens, tokenType);
}
} else if (character < 256) {
if (tokenType != Token.LATIN && sb.length() > 0) {
addToken(sb, tokens, tokenType);
}
tokenType = Token.LATIN;
sb.append(character);
} else {
Token t = getToken(character);
if (t.type == Token.PINYIN) {
if (sb.length() > 0) {
addToken(sb, tokens, tokenType);
}
tokens.add(t);
tokenType = Token.PINYIN;
} else {
if (tokenType != t.type && sb.length() > 0) {
addToken(sb, tokens, tokenType);
}
tokenType = t.type;
sb.append(character);
}
}
}
if (sb.length() > 0) {
addToken(sb, tokens, tokenType);
}
return tokens;
}
private void addToken(
final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) {
String str = sb.toString();
tokens.add(new Token(tokenType, str, str));
sb.setLength(0);
}
public String toPinyinString(String string) {
if (string == null) {
return null;
}
StringBuilder sb = new StringBuilder();
ArrayList<Token> tokens = get(string);
for (Token token : tokens) {
sb.append(token.target);
}
return sb.toString().toLowerCase();
}
public static class Token {
/**
* Separator between target string for each source char
*/
public static final String SEPARATOR = " ";
public static final int LATIN = 1;
public static final int PINYIN = 2;
public static final int UNKNOWN = 3;
/**
* Type of this token, ASCII, PINYIN or UNKNOWN.
*/
public int type;
/**
* Original string before translation.
*/
public String source;
/**
* Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is
* original string in source.
*/
public String target;
public Token() {
}
public Token(int type, String source, String target) {
this.type = type;
this.source = source;
this.target = target;
}
}
}

View file

@ -0,0 +1,36 @@
package me.bmax.apatch.util
import android.content.ContentResolver
import android.net.Uri
import me.bmax.apatch.apApp
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
val cr: ContentResolver get() = apApp.contentResolver
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
fun Uri.fileDescriptor(mode: String) =
cr.openFileDescriptor(this, mode) ?: throw FileNotFoundException()
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,
outStream: Out,
withBoth: (In, Out) -> Unit
) {
inStream.use { reader ->
outStream.use { writer ->
withBoth(reader, writer)
}
}
}
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) }

View file

@ -0,0 +1,5 @@
package me.bmax.apatch.util
data class LatestVersionInfo(
val versionCode: Int = 0, val downloadUrl: String = "", val changelog: String = ""
)

View file

@ -0,0 +1,94 @@
package me.bmax.apatch.util
import android.content.Context
import android.os.Build
import android.system.Os
import com.topjohnwu.superuser.ShellUtils
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun getBugreportFile(context: Context): File {
val bugreportDir = File(context.cacheDir, "bugreport")
bugreportDir.mkdirs()
val dmesgFile = File(bugreportDir, "dmesg.txt")
val logcatFile = File(bugreportDir, "logcat.txt")
val tombstonesFile = File(bugreportDir, "tombstones.tar.gz")
val dropboxFile = File(bugreportDir, "dropbox.tar.gz")
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
val diagFile = File(bugreportDir, "diag.tar.gz")
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
val mountsFile = File(bugreportDir, "mounts.txt")
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
val apFileTree = File(bugreportDir, "ap_tree.txt")
val appListFile = File(bugreportDir, "packages.txt")
val propFile = File(bugreportDir, "props.txt")
val packageConfigFile = File(bugreportDir, "package_config")
val shell = tryGetRootShell()
shell.newJob().add("dmesg > ${dmesgFile.absolutePath}").exec()
shell.newJob().add("logcat -d > ${logcatFile.absolutePath}").exec()
shell.newJob().add("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec()
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag .").exec()
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ap/log .").exec()
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec()
shell.newJob().add("ls -alRZ /data/adb > ${apFileTree.absolutePath}").exec()
shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec()
shell.newJob().add("getprop > ${propFile.absolutePath}").exec()
shell.newJob().add("cp /data/adb/ap/package_config ${packageConfigFile.absolutePath}").exec()
val selinux = ShellUtils.fastCmd(shell, "getenforce")
// basic information
val buildInfo = File(bugreportDir, "basic.txt")
PrintWriter(FileWriter(buildInfo)).use { pw ->
pw.println("Kernel: ${System.getProperty("os.version")}")
pw.println("BRAND: " + Build.BRAND)
pw.println("MODEL: " + Build.MODEL)
pw.println("PRODUCT: " + Build.PRODUCT)
pw.println("MANUFACTURER: " + Build.MANUFACTURER)
pw.println("SDK: " + Build.VERSION.SDK_INT)
pw.println("PREVIEW_SDK: " + Build.VERSION.PREVIEW_SDK_INT)
pw.println("FINGERPRINT: " + Build.FINGERPRINT)
pw.println("DEVICE: " + Build.DEVICE)
pw.println("Manager: " + Version.getManagerVersion())
pw.println("SELinux: $selinux")
val uname = Os.uname()
pw.println("KernelRelease: ${uname.release}")
pw.println("KernelVersion: ${uname.version}")
pw.println("Mahcine: ${uname.machine}")
pw.println("Nodename: ${uname.nodename}")
pw.println("Sysname: ${uname.sysname}")
pw.println("KPatch: ${Version.installedKPVString()}")
pw.println("APatch: ${Version.installedApdVString}")
val safeMode = false
pw.println("SafeMode: $safeMode")
}
// modules
val modulesFile = File(bugreportDir, "modules.json")
modulesFile.writeText(listModules())
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
val targetFile = File(context.cacheDir, "APatch_bugreport_${current}.tar.gz")
shell.newJob().add("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .")
.exec()
shell.newJob().add("rm -rf ${bugreportDir.absolutePath}").exec()
shell.newJob().add("chmod 0644 ${targetFile.absolutePath}").exec()
return targetFile
}

View file

@ -0,0 +1,91 @@
package me.bmax.apatch.util
import android.os.Parcelable
import android.util.Log
import androidx.annotation.Keep
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
import me.bmax.apatch.APApplication
import me.bmax.apatch.Natives
import java.io.File
import java.io.FileWriter
import kotlin.concurrent.thread
object PkgConfig {
private const val TAG = "PkgConfig"
private const val CSV_HEADER = "pkg,exclude,allow,uid,to_uid,sctx"
@Immutable
@Parcelize
@Keep
data class Config(
var pkg: String = "", var exclude: Int = 0, var allow: Int = 0, var profile: Natives.Profile
) : Parcelable {
companion object {
fun fromLine(line: String): Config {
val sp = line.split(",")
val profile = Natives.Profile(sp[3].toInt(), sp[4].toInt(), sp[5])
return Config(sp[0], sp[1].toInt(), sp[2].toInt(), profile)
}
}
fun isDefault(): Boolean {
return allow == 0 && exclude == 0
}
fun toLine(): String {
return "${pkg},${exclude},${allow},${profile.uid},${profile.toUid},${profile.scontext}"
}
}
fun readConfigs(): HashMap<Int, Config> {
val configs = HashMap<Int, Config>()
val file = File(APApplication.PACKAGE_CONFIG_FILE)
if (file.exists()) {
file.readLines().drop(1).filter { it.isNotEmpty() }.forEach {
Log.d(TAG, it)
val p = Config.fromLine(it)
if (!p.isDefault()) {
configs[p.profile.uid] = p
}
}
}
return configs
}
private fun writeConfigs(configs: HashMap<Int, Config>) {
val file = File(APApplication.PACKAGE_CONFIG_FILE)
if (!file.parentFile?.exists()!!) file.parentFile?.mkdirs()
val writer = FileWriter(file, false)
writer.write(CSV_HEADER + '\n')
configs.values.forEach {
if (!it.isDefault()) {
writer.write(it.toLine() + '\n')
}
}
writer.flush()
writer.close()
}
fun changeConfig(config: Config) {
thread {
synchronized(PkgConfig.javaClass) {
Natives.su()
val configs = readConfigs()
val uid = config.profile.uid
// Root App should not be excluded
if (config.allow == 1) {
config.exclude = 0
}
if (config.allow == 0 && configs[uid] != null && config.exclude != 0) {
configs.remove(uid)
} else {
Log.d(TAG, "change config: $config")
configs[uid] = config
}
writeConfigs(configs)
}
}
}
}

View file

@ -0,0 +1,153 @@
package me.bmax.apatch.util
import android.util.Log
import androidx.core.content.pm.PackageInfoCompat
import me.bmax.apatch.APApplication
import me.bmax.apatch.BuildConfig
import me.bmax.apatch.Natives
import me.bmax.apatch.apApp
import me.bmax.apatch.util.shellForResult
import org.ini4j.Ini
import java.io.StringReader
import me.bmax.apatch.ui.viewmodel.KPModel
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import com.topjohnwu.superuser.Shell
import androidx.compose.runtime.mutableStateOf
import java.io.File
import android.system.Os
/**
* version string is like 0.9.0 or 0.9.0-dev
* version uint is hex number like: 0x000900
*/
object Version {
private fun string2UInt(ver: String): UInt {
val v = ver.trim().split("-")[0]
val vn = v.split('.')
val vi = vn[0].toInt().shl(16) + vn[1].toInt().shl(8) + vn[2].toInt()
return vi.toUInt()
}
fun getKpImg(): String {
var shell: Shell = createRootShell()
var kimgInfo = mutableStateOf(KPModel.KImgInfo("", false))
var kpimgInfo = mutableStateOf(KPModel.KPImgInfo("", "", "", "", ""))
val patchDir: ExtendedFile = FileSystemManager.getLocal().getFile(apApp.filesDir.parent, "check")
patchDir.deleteRecursively()
patchDir.mkdirs()
val execs = listOf(
"libkptools.so", "libmagiskboot.so", "libbusybox.so"
)
val info = apApp.applicationInfo
val libs = File(info.nativeLibraryDir).listFiles { _, name ->
execs.contains(name)
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
Os.symlink(lib.path, "$patchDir/$name")
}
for (script in listOf(
"boot_patch.sh", "boot_unpatch.sh", "boot_extract.sh", "util_functions.sh", "kpimg"
)) {
val dest = File(patchDir, script)
apApp.assets.open(script).writeTo(dest)
}
val result = shellForResult(
shell, "cd $patchDir", "./kptools -l -k kpimg"
)
if (result.isSuccess) {
val ini = Ini(StringReader(result.out.joinToString("\n")))
val kpimg = ini["kpimg"]
if (kpimg != null) {
kpimgInfo.value = KPModel.KPImgInfo(
kpimg["version"].toString(),
kpimg["compile_time"].toString(),
kpimg["config"].toString(),
APApplication.superKey, // current key
kpimg["root_superkey"].toString() // possibly empty
)
return kpimg["compile_time"].toString()
}
}
return "unknown"
}
fun uInt2String(ver: UInt): String {
return "%d.%d.%d".format(
ver.and(0xff0000u).shr(16).toInt(),
ver.and(0xff00u).shr(8).toInt(),
ver.and(0xffu).toInt()
)
}
fun installedKPTime(): String {
val time = Natives.kernelPatchBuildTime()
return if (time.startsWith("ERROR_")) "读取失败" else time
}
fun buildKPVUInt(): UInt {
val buildVS = BuildConfig.buildKPV
return string2UInt(buildVS)
}
fun buildKPVString(): String {
return BuildConfig.buildKPV
}
/**
* installed KernelPatch version (installed kpimg)
*/
fun installedKPVUInt(): UInt {
return Natives.kernelPatchVersion().toUInt()
}
fun installedKPVString(): String {
return uInt2String(installedKPVUInt())
}
private fun installedKPatchVString(): String {
val resultShell = rootShellForResult("${APApplication.APD_PATH} -V")
val result = resultShell.out.toString()
return result.trim().ifEmpty { "0" }
}
fun installedKPatchVUInt(): UInt {
return installedKPatchVString().trim().toUInt(0x10)
}
private fun installedApdVString(): String {
val resultShell = rootShellForResult("${APApplication.APD_PATH} -V")
installedApdVString = if (resultShell.isSuccess) {
val result = resultShell.out.toString()
Log.i("APatch", "[installedApdVString@Version] resultFromShell: $result")
Regex("\\d+").find(result)?.value ?: "0"
} else {
"0"
}
return installedApdVString
}
fun installedApdVUInt(): Int {
installedApdVInt = installedApdVString().toInt()
return installedApdVInt
}
fun getManagerVersion(): Pair<String, Long> {
val packageInfo = apApp.packageManager.getPackageInfo(apApp.packageName, 0)!!
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
return Pair(packageInfo.versionName!!, versionCode)
}
var installedApdVInt: Int = 0
var installedApdVString: String = "0"
}

View file

@ -0,0 +1,104 @@
package me.bmax.apatch.util.ui
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.os.Build
import android.util.Log
import android.view.SurfaceControl
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.view.animation.DecelerateInterpolator
import java.lang.reflect.Method
open class APDialogBlurBehindUtils {
companion object {
private val bIsBlurSupport =
getSystemProperty("ro.surface_flinger.supports_background_blur") && !getSystemProperty("persist.sys.sf.disable_blurs")
private fun getSystemProperty(key: String?): Boolean {
var value = false
try {
val c = Class.forName("android.os.SystemProperties")
val get = c.getMethod(
"getBoolean", String::class.java, Boolean::class.javaPrimitiveType
)
value = get.invoke(c, key, false) as Boolean
} catch (e: Exception) {
Log.e("APatchUI", "[APDialogBlurBehindUtils] Failed to getSystemProperty: ", e)
}
return value
}
private fun updateWindowForBlurs(window: Window, blursEnabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window.setDimAmount(0.27f)
window.attributes.blurBehindRadius = 20
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (blursEnabled) {
val view = window.decorView
val animator = ValueAnimator.ofInt(1, 53)
animator.duration = 667
animator.interpolator = DecelerateInterpolator()
try {
val viewRootImpl =
view.javaClass.getMethod("getViewRootImpl").invoke(view) ?: return
val surfaceControl = viewRootImpl.javaClass.getMethod("getSurfaceControl")
.invoke(viewRootImpl) as SurfaceControl
@SuppressLint("BlockedPrivateApi") val setBackgroundBlurRadius: Method =
SurfaceControl.Transaction::class.java.getDeclaredMethod(
"setBackgroundBlurRadius",
SurfaceControl::class.java,
Int::class.javaPrimitiveType
)
animator.addUpdateListener { animation: ValueAnimator ->
try {
val transaction = SurfaceControl.Transaction()
val animatedValue = animation.animatedValue
if (animatedValue != null) {
setBackgroundBlurRadius.invoke(
transaction, surfaceControl, animatedValue as Int
)
}
transaction.apply()
} catch (t: Throwable) {
Log.e(
"APatchUI",
"[APDialogBlurBehindUtils] Blur behind dialog builder: " + t.toString()
)
}
}
} catch (t: Throwable) {
Log.e(
"APatchUI",
"[APDialogBlurBehindUtils] Blur behind dialog builder: " + t.toString()
)
}
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
animator.cancel()
}
})
animator.start()
}
}
}
fun setupWindowBlurListener(window: Window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window.setFlags(
WindowManager.LayoutParams.FLAG_BLUR_BEHIND,
WindowManager.LayoutParams.FLAG_BLUR_BEHIND
)
updateWindowForBlurs(window, true)
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
updateWindowForBlurs(
window, bIsBlurSupport
)
}
}
}
}

View file

@ -0,0 +1,8 @@
package me.bmax.apatch.util.ui
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
error("CompositionLocal LocalSnackbarController not present")
}

View file

@ -0,0 +1,87 @@
package me.bmax.apatch.util.ui
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import java.util.regex.Pattern
@Composable
fun LinkifyText(
text: String,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
val layoutResult = remember {
mutableStateOf<TextLayoutResult?>(null)
}
val linksList = extractUrls(text)
val annotatedString = buildAnnotatedString {
append(text)
linksList.forEach {
addStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
),
start = it.start,
end = it.end
)
addStringAnnotation(
tag = "URL",
annotation = it.url,
start = it.start,
end = it.end
)
}
}
Text(
text = annotatedString,
modifier = modifier.pointerInput(Unit) {
detectTapGestures { offsetPosition ->
layoutResult.value?.let {
val position = it.getOffsetForPosition(offsetPosition)
annotatedString.getStringAnnotations(position, position).firstOrNull()
?.let { result ->
if (result.tag == "URL") {
uriHandler.openUri(result.item)
}
}
}
}
},
onTextLayout = { layoutResult.value = it }
)
}
private val urlPattern: Pattern = Pattern.compile(
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)"
+ "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*"
+ "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
)
private data class LinkInfo(
val url: String,
val start: Int,
val end: Int
)
private fun extractUrls(text: String): List<LinkInfo> = buildList {
val matcher = urlPattern.matcher(text)
while (matcher.find()) {
val matchStart = matcher.start(1)
val matchEnd = matcher.end()
val url = text.substring(matchStart, matchEnd).replaceFirst("http://", "https://")
add(LinkInfo(url, matchStart, matchEnd))
}
}

View file

@ -0,0 +1,23 @@
package me.bmax.apatch.util.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun NavigationBarsSpacer(
modifier: Modifier = Modifier
) {
val paddingValues = WindowInsets.navigationBars.asPaddingValues()
Box(
modifier = Modifier.padding(paddingValues)
) {
Spacer(modifier = modifier)
}
}

View file

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.5,21h-4.5a2,2 0,0 1,-2 -2v-14a2,2 0,0 1,2 -2h8a2,2 0,0 1,2 2v7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M11,4h2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,17v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M19,16v6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M22,19l-3,3l-3,-3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#ffff"
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group android:scaleX="0.97"
android:scaleY="0.97"
android:translateX="2.88"
android:translateY="2.88">
<path
android:pathData="M0 0L192 0L192 192L0 192L0 0Z"
android:fillColor="#FFFFFF"
android:strokeColor="#FFFFFF"
android:strokeWidth="1" />
</group>
</vector>

View file

@ -0,0 +1,108 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.7135135"
android:scaleY="0.7135135"
android:translateX="73.34054"
android:translateY="73.34054">
<path
android:pathData="M304.42,123.08Q319,99.08 322,92.18a7.08,7.08 0,0 0,-1.31 -7.8,3.79 3.79,0 0,0 -3.18,-1.44c-3.51,0.12 -5.65,-0.63 -7.93,2.69a60.66,60.66 0,0 0,-5.37 9.64c-3.33,7.48 -7.67,14.36 -11.54,21.59a0.86,0.86 0,0 1,-1.07 0.39c-14.4,-5.52 -30.47,-7.17 -45.76,-6a94.62,94.62 0,0 0,-26 6,1.15 1.15,0 0,1 -1.41,-0.5c-3.84,-6.48 -7.19,-12.44 -10.77,-20 -2.1,-4.42 -4.6,-9.64 -8.21,-13.08a1.81,1.81 0,0 0,-1 -0.49c-4.23,-0.57 -9.27,-0.79 -9.95,4.78a12.35,12.35 0,0 0,1.87 7.43q7.75,14 15.7,27.43a1.21,1.21 0,0 1,-0.43 1.64c-26.65,15.7 -44,42.33 -43.91,73.42a0.67,0.67 0,0 0,0.67 0.67l186.39,-0.1a1.39,1.39 0,0 0,1.39 -1.4v-0.08c-0.76,-11.39 -2.94,-22.68 -8,-33.18q-12,-25.2 -37.52,-40a0.45,0.45 0,0 1,-0.18 -0.69"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M136.32,330.27A21.63,21.63 0,0 0,156 309.12q0,-43.94 0,-86c0,-5.5 -1.13,-10.51 -4.75,-14.87a22.21,22.21 0,0 0,-35.45 1.63,9.93 9.93,0 0,0 -1.81,5.86q0,49.15 0,98.85a10.64,10.64 0,0 0,1.8 6q7.55,10.76 20.5,9.7"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M398.87,221.61a21.3,21.3 0,0 0,-21.33 -21.26h-1.48a21.3,21.3 0,0 0,-21.27 21.34l0.16,87.22a21.3,21.3 0,0 0,21.33 21.26h1.48A21.3,21.3 0,0 0,399 308.83l-0.16,-87.22"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M243.08,363l24.62,-0.05a0.45,0.45 0,0 1,0.52 0.51q0.13,17.21 -0.06,34.75 -0.14,11 1.41,16 2.76,8.84 12.66,13.73c3,1.5 7.25,1.17 10.62,1.18a15,15 0,0 0,9.09 -3q10,-7.66 10.08,-20.67 0.14,-19.67 -0.07,-41.42c0,-0.76 0.36,-1.12 1.13,-1.09a115.4,115.4 0,0 0,11.91 0c16.94,-0.9 25.13,-12.07 25.11,-28.21q-0.06,-64.95 0,-129.87a0.8,0.8 0,0 0,-0.8 -0.81h-187a0.83,0.83 0,0 0,-0.82 0.83h0q0.35,61.24 0.08,128c0,9.74 0.26,15.67 6.6,22.33 8.58,9 19.77,8 31.25,7.75a0.42,0.42 0,0 1,0.42 0.41q0,21.4 -0.08,43.79a22.1,22.1 0,0 0,1.47 8.58c2.18,5.27 8.63,12.65 15.17,13.15 4.89,0.37 10.12,0.7 14.42,-1.79 7.15,-4.16 12,-10.91 11.91,-19.12q-0.18,-22.17 -0.09,-44.46a0.48,0.48 0,0 1,0.54 -0.54"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M202.24,142.7a73.29,73.29 0,0 0,-26 41.57,0.6 0.6,0 0,0 0.46,0.71h0.12q78.3,0 156.93,0a3,3 0,0 0,1 -0.16,0.52 0.52,0 0,0 0.39,-0.7c-2.83,-12 -8.39,-23.5 -16.83,-32.58a93.87,93.87 0,0 0,-21.1 -17.25,63.89 63.89,0 0,0 -13.06,-5.64 98.42,98.42 0,0 0,-13.78 -3.44,89.93 89.93,0 0,0 -45.77,4.37 61,61 0,0 0,-13.44 6.6q-4.54,3.09 -8.86,6.55"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M143,222.08a8.63,8.63 0,0 0,-8.63 -8.63H134a8.63,8.63 0,0 0,-8.63 8.63h0V308.7a8.63,8.63 0,0 0,8.63 8.63h0.3A8.63,8.63 0,0 0,143 308.7V222.08"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M385.51,221.45a7.87,7.87 0,0 0,-7.84 -7.9h-1a7.87,7.87 0,0 0,-7.9 7.84l-0.3,88a7.87,7.87 0,0 0,7.84 7.9h1a7.87,7.87 0,0 0,7.9 -7.84l0.3,-88"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M204.46,350.63c5.26,1.85 7.52,8.11 7.51,13.3q0,18.68 0,40.92a18.92,18.92 0,0 0,0.66 5A8.41,8.41 0,0 0,225 415q5,-2.91 5,-10.15 -0.07,-20.33 0,-42.76 0,-7.64 6.29,-11.18a17.67,17.67 0,0 1,7.48 -1.69q12.3,0 23.73,0a5.94,5.94 0,0 1,1.77 0.27c1.44,0.45 3,0.49 4.37,1.13q7.94,3.69 7.85,12.49 -0.25,23.72 0.25,43.09a10.52,10.52 0,0 0,3.45 7.89,8.43 8.43,0 0,0 13.05,-2.88 13.45,13.45 0,0 0,1 -5.33q-0.1,-20.39 0,-39.88c0,-6.48 2.17,-13.84 9,-15.7a24,24 0,0 1,6 -1.08q5.76,0 11.39,0.07a10.06,10.06 0,0 0,8.7 -3.85c2.38,-3.09 2.06,-7.7 2,-11.44q-0.24,-50.9 0,-100.2 0,-8.13 0.52,-16.54a0.48,0.48 0,0 0,-0.45 -0.51h-161a0.48,0.48 0,0 0,-0.49 0.48h0q0,59.14 -0.07,118.94c0,5.08 0.71,9.07 5.52,11.79a12.21,12.21 0,0 0,6.63 1.36c5.86,-0.24 12,-0.69 17.6,1.26"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M256.14,241.06c2.47,1.21 4.51,5.41 5.77,7.73a1.87,1.87 0,0 0,2.66 0.93l5.18,-2.23a1.7,1.7 0,0 0,1 -2.45,28.37 28.37,0 0,0 -8.27,-11.56c-9.1,-7.71 -17.26,1.13 -21.57,9.18a14.06,14.06 0,0 0,-1.4 3.83,1 1,0 0,0 0.29,0.94c1.51,1.49 4.46,2.26 6.39,3a2.41,2.41 0,0 0,3.13 -1.35l0.05,-0.14a19.08,19.08 0,0 1,4.66 -7.56,1.83 1.83,0 0,1 2.08,-0.34"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M244,262.69a0.45,0.45 0,0 1,0.2 0.6,0.4 0.4,0 0,1 -0.16,0.18c-8.73,5.77 -16.12,12.75 -23.42,20.14a77.72,77.72 0,0 0,-9.36 11.74c-5.51,8.31 -10.38,26.49 5.94,27.34 11,0.56 23.33,-4.52 33.21,-9.79 5.46,-2.91 10.75,-6.8 16,-10.29C279.23,294 293.2,281.16 301.47,267a24.68,24.68 0,0 0,2.5 -16.87c-0.84,-4.13 -6.55,-6.3 -10.17,-6.6a40.62,40.62 0,0 0,-15.52 2.07,98.9 98.9,0 0,0 -22.7,10.19 0.79,0.79 0,0 1,-0.84 0,136.77 136.77,0 0,0 -18.21,-8.14c-8.95,-3.25 -29.8,-8.43 -30.71,7.3 -0.2,3.48 1,6.81 2.12,10.08a8,8 0,0 1,0.25 4.07c-0.89,4.78 0.63,9.48 5.19,11.62q6.41,3 11.39,-1.84c6.58,-6.38 3.13,-16.56 -5.94,-17.65A3.7,3.7 0,0 1,215.6 257a2.54,2.54 0,0 1,2.53 -2.4c8,-0.67 18.37,4.28 25.87,8.09"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M295.13,307.57c0,0.63 0,1.25 0,1.88a2.06,2.06 0,0 1,-2.13 2.28c-8.24,0.66 -17.51,-3.69 -24.95,-7.42a2.45,2.45 0,0 0,-2.4 0.11l-6.94,4.3a1.72,1.72 0,0 0,-0.56 2.37,1.7 1.7,0 0,0 0.64,0.61 89.73,89.73 0,0 0,28.29 10.15c4.74,0.81 13.83,0.63 16.4,-4.11 3,-5.59 1.52,-10.36 0,-17 -0.66,-2.82 0.13,-5.78 -0.93,-8.44 -1.8,-4.51 -6.79,-8.14 -11.84,-6.81 -5.57,1.47 -10.46,7.66 -7.77,13.69q3.13,7 10.69,7a1.32,1.32 0,0 1,1.51 1.43"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M258.74,323c-4.24,6 -8,-2.7 -9.08,-5.69a1.92,1.92 0,0 0,-2.24 -1.23,21.29 21.29,0 0,0 -6.63,2.68 1.86,1.86 0,0 0,-0.8 2.55c3.17,7.55 12,19.16 21.61,12.47q5.94,-4.15 9.42,-12.37a2.53,2.53 0,0 0,-1.44 -3.69l-4.16,-1.88a2.34,2.34 0,0 0,-3.54 1.39,30.07 30.07,0 0,1 -3.14,5.77"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M252.39,295.58a1.36,1.36 0,0 1,-0.44 1.57,65 65,0 0,1 -6.55,4.29 2.69,2.69 0,0 1,-3.07 -0.08,124 124,0 0,1 -12.82,-10.07 0.93,0.93 0,0 0,-1.29 0c-4,4 -12.35,13 -12.18,18.91a2.06,2.06 0,0 0,1.92 2,26.58 26.58,0 0,0 11.29,-1.58 121.66,121.66 0,0 0,24.36 -12.43,212.63 212.63,0 0,0 25.07,-19.73c6,-5.5 14,-13.41 16,-21.4 1.65,-6.65 -13.58,-1.58 -16.06,-0.51q-6,2.61 -12.34,5.71a0.54,0.54 0,0 0,-0.25 0.71,0.64 0.64,0 0,0 0.19,0.21 137.63,137.63 0,0 1,12.46 9.29,2 2,0 0,1 0.3,3.15 21.39,21.39 0,0 1,-6 5.14,2.06 2.06,0 0,1 -2.34,-0.21 134.52,134.52 0,0 0,-14.92 -10.77,0.78 0.78,0 0,0 -0.9,0 157.9,157.9 0,0 0,-17.7 13.43,0.33 0.33,0 0,0 0,0.45l0.06,0.05q7.53,5.38 14.56,10.95a2.16,2.16 0,0 1,0.68 0.9"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M261.87,275a8.83,8.83 0,0 0,-12.49 0l-2,2a8.82,8.82 0,0 0,0 12.48h0l2,2a8.83,8.83 0,0 0,12.49 0l2,-2a8.82,8.82 0,0 0,0 -12.48h0l-2,-2"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#237c4b"/>
<path
android:pathData="M304.6,123.77q25.48,14.78 37.52,40c5,10.5 7.2,21.79 8,33.18a1.4,1.4 0,0 1,-1.31 1.48h-0.08l-186.39,0.1a0.67,0.67 0,0 1,-0.67 -0.67c-0.09,-31.09 17.26,-57.72 43.91,-73.42a1.21,1.21 0,0 0,0.43 -1.64q-7.93,-13.45 -15.7,-27.43a12.35,12.35 0,0 1,-1.87 -7.43c0.68,-5.57 5.72,-5.35 9.95,-4.78a1.81,1.81 0,0 1,1 0.49c3.61,3.44 6.11,8.66 8.21,13.08 3.58,7.57 6.93,13.53 10.77,20a1.15,1.15 0,0 0,1.41 0.5,94.62 94.62,0 0,1 26,-6c15.29,-1.15 31.36,0.5 45.76,6a0.86,0.86 0,0 0,1.07 -0.39c3.87,-7.23 8.21,-14.11 11.54,-21.59a60.66,60.66 0,0 1,5.37 -9.64c2.28,-3.32 4.42,-2.57 7.93,-2.69a3.79,3.79 0,0 1,3.18 1.44,7.08 7.08,0 0,1 1.31,7.8q-3,6.9 -17.56,30.9A0.45,0.45 0,0 0,304.6 123.77ZM202.24,142.7a73.29,73.29 0,0 0,-26 41.57,0.6 0.6,0 0,0 0.46,0.71h0.12q78.3,0 156.93,0a3,3 0,0 0,1 -0.16,0.52 0.52,0 0,0 0.39,-0.7c-2.83,-12 -8.39,-23.5 -16.83,-32.58a93.87,93.87 0,0 0,-21.1 -17.25,63.89 63.89,0 0,0 -13.06,-5.64 98.42,98.42 0,0 0,-13.78 -3.44,89.93 89.93,0 0,0 -45.77,4.37 61,61 0,0 0,-13.44 6.6Q206.56,139.24 202.24,142.7Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M156,309.12a21.63,21.63 0,0 1,-19.68 21.15q-13,1.07 -20.5,-9.7a10.64,10.64 0,0 1,-1.8 -6q0,-49.69 0,-98.85a9.93,9.93 0,0 1,1.81 -5.86,22.21 22.21,0 0,1 35.45,-1.63c3.62,4.36 4.75,9.37 4.75,14.87Q156,265.18 156,309.12ZM142.95,222.12a8.63,8.63 0,0 0,-8.63 -8.63L134,213.49a8.63,8.63 0,0 0,-8.63 8.63h0L125.37,308.7a8.63,8.63 0,0 0,8.63 8.63h0.3A8.63,8.63 0,0 0,143 308.7Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M399,308.83a21.3,21.3 0,0 1,-21.27 21.34h-1.48A21.3,21.3 0,0 1,355 308.91h0l-0.16,-87.22a21.3,21.3 0,0 1,21.27 -21.34h1.48a21.3,21.3 0,0 1,21.33 21.26h0ZM385.48,221.45a7.87,7.87 0,0 0,-7.84 -7.9h-1a7.87,7.87 0,0 0,-7.9 7.84l-0.3,88a7.87,7.87 0,0 0,7.84 7.9h1a7.87,7.87 0,0 0,7.9 -7.84Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M242.54,363.52q-0.09,22.29 0.09,44.46c0.07,8.21 -4.76,15 -11.91,19.12 -4.3,2.49 -9.53,2.16 -14.42,1.79 -6.54,-0.5 -13,-7.88 -15.17,-13.15a22.1,22.1 0,0 1,-1.47 -8.58q0.12,-22.4 0.08,-43.79a0.42,0.42 0,0 0,-0.42 -0.41c-11.48,0.22 -22.67,1.25 -31.25,-7.75 -6.34,-6.66 -6.64,-12.59 -6.6,-22.33q0.27,-66.81 -0.08,-128a0.83,0.83 0,0 1,0.82 -0.83h187a0.8,0.8 0,0 1,0.8 0.81q0,64.92 0,129.87c0,16.14 -8.17,27.31 -25.11,28.21a115.4,115.4 0,0 1,-11.91 0c-0.77,0 -1.14,0.33 -1.13,1.09q0.19,21.76 0.07,41.42 -0.09,13 -10.08,20.67a15,15 0,0 1,-9.09 3c-3.37,0 -7.6,0.32 -10.62,-1.18q-9.9,-4.9 -12.66,-13.73 -1.54,-4.92 -1.41,-16 0.19,-17.55 0.06,-34.75a0.45,0.45 0,0 0,-0.52 -0.51l-24.62,0.05A0.48,0.48 0,0 0,242.54 363.52ZM204.46,350.63c5.26,1.85 7.52,8.11 7.51,13.3q0,18.68 0,40.92a18.92,18.92 0,0 0,0.66 5A8.41,8.41 0,0 0,225 415q5,-2.91 5,-10.15 -0.07,-20.33 0,-42.76 0,-7.64 6.29,-11.18a17.67,17.67 0,0 1,7.48 -1.69q12.3,0 23.73,0a5.94,5.94 0,0 1,1.77 0.27c1.44,0.45 3,0.49 4.37,1.13q7.94,3.69 7.85,12.49 -0.25,23.72 0.25,43.09a10.52,10.52 0,0 0,3.45 7.89,8.43 8.43,0 0,0 13.05,-2.88 13.45,13.45 0,0 0,1 -5.33q-0.1,-20.39 0,-39.88c0,-6.48 2.17,-13.84 9,-15.7a24,24 0,0 1,6 -1.08q5.76,0 11.39,0.07a10.06,10.06 0,0 0,8.7 -3.85c2.38,-3.09 2.06,-7.7 2,-11.44q-0.24,-50.9 0,-100.2 0,-8.13 0.52,-16.54a0.48,0.48 0,0 0,-0.45 -0.51h-161a0.48,0.48 0,0 0,-0.49 0.48h0q0,59.14 -0.07,118.94c0,5.08 0.71,9.07 5.52,11.79a12.21,12.21 0,0 0,6.63 1.36C192.72,349.13 198.89,348.68 204.46,350.63Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M256.14,241.06a1.83,1.83 0,0 0,-2.08 0.34A19.08,19.08 0,0 0,249.4 249a2.42,2.42 0,0 1,-3 1.54l-0.14,-0.05c-1.93,-0.76 -4.88,-1.53 -6.39,-3a1,1 0,0 1,-0.29 -0.94,14.06 14.06,0 0,1 1.4,-3.83c4.31,-8 12.47,-16.89 21.57,-9.18A28.37,28.37 0,0 1,270.78 245a1.7,1.7 0,0 1,-1 2.45l-5.18,2.23a1.87,1.87 0,0 1,-2.66 -0.93C260.65,246.47 258.61,242.27 256.14,241.06Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M244,262.69c-7.5,-3.81 -17.85,-8.76 -25.87,-8.09a2.54,2.54 0,0 0,-2.53 2.4,3.7 3.7,0 0,0 3.23,4.21c9.07,1.09 12.52,11.27 5.94,17.65q-5,4.83 -11.39,1.84c-4.56,-2.14 -6.08,-6.84 -5.19,-11.62a8,8 0,0 0,-0.25 -4.07c-1.1,-3.27 -2.32,-6.6 -2.12,-10.08 0.91,-15.73 21.76,-10.55 30.71,-7.3a136.77,136.77 0,0 1,18.21 8.14,0.79 0.79,0 0,0 0.84,0 98.9,98.9 0,0 1,22.7 -10.19,40.62 40.62,0 0,1 15.52,-2.07c3.62,0.3 9.33,2.47 10.17,6.6a24.68,24.68 0,0 1,-2.5 16.87c-8.27,14.21 -22.24,27.08 -35.09,35.66 -5.22,3.49 -10.51,7.38 -16,10.29 -9.88,5.27 -22.24,10.35 -33.21,9.79 -16.32,-0.85 -11.45,-19 -5.94,-27.34a77.72,77.72 0,0 1,9.36 -11.74c7.3,-7.39 14.69,-14.37 23.42,-20.14a0.45,0.45 0,0 0,0.14 -0.62A0.55,0.55 0,0 0,244 262.69ZM252.39,295.58a1.36,1.36 0,0 1,-0.44 1.57,65 65,0 0,1 -6.55,4.29 2.69,2.69 0,0 1,-3.07 -0.08,124 124,0 0,1 -12.82,-10.07 0.93,0.93 0,0 0,-1.29 0c-4,4 -12.35,13 -12.18,18.91a2.06,2.06 0,0 0,1.92 2,26.58 26.58,0 0,0 11.29,-1.58 121.66,121.66 0,0 0,24.36 -12.43,212.63 212.63,0 0,0 25.07,-19.73c6,-5.5 14,-13.41 16,-21.4 1.65,-6.65 -13.58,-1.58 -16.06,-0.51q-6,2.61 -12.34,5.71a0.54,0.54 0,0 0,-0.25 0.71,0.64 0.64,0 0,0 0.19,0.21 137.63,137.63 0,0 1,12.46 9.29,2 2,0 0,1 0.3,3.15 21.39,21.39 0,0 1,-6 5.14,2.06 2.06,0 0,1 -2.34,-0.21 134.52,134.52 0,0 0,-14.92 -10.77,0.78 0.78,0 0,0 -0.9,0 157.9,157.9 0,0 0,-17.7 13.43,0.33 0.33,0 0,0 0,0.45l0.06,0.05q7.53,5.38 14.56,10.95A2.16,2.16 0,0 1,252.39 295.58Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M247.42,276.94L249.39,274.98A8.83,8.83 90.2,0 1,261.88 275.02L263.88,277.04A8.83,8.83 90.2,0 1,263.83 289.53L261.86,291.49A8.83,8.83 90.2,0 1,249.37 291.44L247.37,289.43A8.83,8.83 90.2,0 1,247.42 276.94z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M293.62,306.14q-7.56,0 -10.69,-7c-2.69,-6 2.2,-12.22 7.77,-13.69 5.05,-1.33 10,2.3 11.84,6.81 1.06,2.66 0.27,5.62 0.93,8.44 1.56,6.67 3.08,11.44 0,17 -2.57,4.74 -11.66,4.92 -16.4,4.11a89.73,89.73 0,0 1,-28.29 -10.15,1.73 1.73,0 0,1 -0.69,-2.34 1.78,1.78 0,0 1,0.61 -0.64l6.94,-4.3a2.45,2.45 0,0 1,2.4 -0.11c7.44,3.73 16.71,8.08 24.95,7.42a2.06,2.06 0,0 0,2.13 -2.28c0,-0.63 0,-1.25 0,-1.88A1.32,1.32 0,0 0,293.62 306.14Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M249.66,317.34c1.05,3 4.84,11.72 9.08,5.69a30.07,30.07 0,0 0,3.14 -5.77,2.34 2.34,0 0,1 3.54,-1.39l4.16,1.88a2.53,2.53 0,0 1,1.44 3.69q-3.48,8.22 -9.42,12.37c-9.59,6.69 -18.44,-4.92 -21.61,-12.47a1.86,1.86 0,0 1,0.8 -2.55,21.29 21.29,0 0,1 6.63,-2.68A1.92,1.92 0,0 1,249.66 317.34Z"
android:fillColor="#3ddc84"/>
</group>
</vector>

View file

@ -0,0 +1,108 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.7135135"
android:scaleY="0.7135135"
android:translateX="73.34054"
android:translateY="73.34054">
<path
android:pathData="M304.42,123.08Q319,99.08 322,92.18a7.08,7.08 0,0 0,-1.31 -7.8,3.79 3.79,0 0,0 -3.18,-1.44c-3.51,0.12 -5.65,-0.63 -7.93,2.69a60.66,60.66 0,0 0,-5.37 9.64c-3.33,7.48 -7.67,14.36 -11.54,21.59a0.86,0.86 0,0 1,-1.07 0.39c-14.4,-5.52 -30.47,-7.17 -45.76,-6a94.62,94.62 0,0 0,-26 6,1.15 1.15,0 0,1 -1.41,-0.5c-3.84,-6.48 -7.19,-12.44 -10.77,-20 -2.1,-4.42 -4.6,-9.64 -8.21,-13.08a1.81,1.81 0,0 0,-1 -0.49c-4.23,-0.57 -9.27,-0.79 -9.95,4.78a12.35,12.35 0,0 0,1.87 7.43q7.75,14 15.7,27.43a1.21,1.21 0,0 1,-0.43 1.64c-26.65,15.7 -44,42.33 -43.91,73.42a0.67,0.67 0,0 0,0.67 0.67l186.39,-0.1a1.39,1.39 0,0 0,1.39 -1.4v-0.08c-0.76,-11.39 -2.94,-22.68 -8,-33.18q-12,-25.2 -37.52,-40a0.45,0.45 0,0 1,-0.18 -0.69"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M136.32,330.27A21.63,21.63 0,0 0,156 309.12q0,-43.94 0,-86c0,-5.5 -1.13,-10.51 -4.75,-14.87a22.21,22.21 0,0 0,-35.45 1.63,9.93 9.93,0 0,0 -1.81,5.86q0,49.15 0,98.85a10.64,10.64 0,0 0,1.8 6q7.55,10.76 20.5,9.7"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M398.87,221.61a21.3,21.3 0,0 0,-21.33 -21.26h-1.48a21.3,21.3 0,0 0,-21.27 21.34l0.16,87.22a21.3,21.3 0,0 0,21.33 21.26h1.48A21.3,21.3 0,0 0,399 308.83l-0.16,-87.22"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M243.08,363l24.62,-0.05a0.45,0.45 0,0 1,0.52 0.51q0.13,17.21 -0.06,34.75 -0.14,11 1.41,16 2.76,8.84 12.66,13.73c3,1.5 7.25,1.17 10.62,1.18a15,15 0,0 0,9.09 -3q10,-7.66 10.08,-20.67 0.14,-19.67 -0.07,-41.42c0,-0.76 0.36,-1.12 1.13,-1.09a115.4,115.4 0,0 0,11.91 0c16.94,-0.9 25.13,-12.07 25.11,-28.21q-0.06,-64.95 0,-129.87a0.8,0.8 0,0 0,-0.8 -0.81h-187a0.83,0.83 0,0 0,-0.82 0.83h0q0.35,61.24 0.08,128c0,9.74 0.26,15.67 6.6,22.33 8.58,9 19.77,8 31.25,7.75a0.42,0.42 0,0 1,0.42 0.41q0,21.4 -0.08,43.79a22.1,22.1 0,0 0,1.47 8.58c2.18,5.27 8.63,12.65 15.17,13.15 4.89,0.37 10.12,0.7 14.42,-1.79 7.15,-4.16 12,-10.91 11.91,-19.12q-0.18,-22.17 -0.09,-44.46a0.48,0.48 0,0 1,0.54 -0.54"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M202.24,142.7a73.29,73.29 0,0 0,-26 41.57,0.6 0.6,0 0,0 0.46,0.71h0.12q78.3,0 156.93,0a3,3 0,0 0,1 -0.16,0.52 0.52,0 0,0 0.39,-0.7c-2.83,-12 -8.39,-23.5 -16.83,-32.58a93.87,93.87 0,0 0,-21.1 -17.25,63.89 63.89,0 0,0 -13.06,-5.64 98.42,98.42 0,0 0,-13.78 -3.44,89.93 89.93,0 0,0 -45.77,4.37 61,61 0,0 0,-13.44 6.6q-4.54,3.09 -8.86,6.55"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M143,222.08a8.63,8.63 0,0 0,-8.63 -8.63H134a8.63,8.63 0,0 0,-8.63 8.63h0V308.7a8.63,8.63 0,0 0,8.63 8.63h0.3A8.63,8.63 0,0 0,143 308.7V222.08"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M385.51,221.45a7.87,7.87 0,0 0,-7.84 -7.9h-1a7.87,7.87 0,0 0,-7.9 7.84l-0.3,88a7.87,7.87 0,0 0,7.84 7.9h1a7.87,7.87 0,0 0,7.9 -7.84l0.3,-88"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M204.46,350.63c5.26,1.85 7.52,8.11 7.51,13.3q0,18.68 0,40.92a18.92,18.92 0,0 0,0.66 5A8.41,8.41 0,0 0,225 415q5,-2.91 5,-10.15 -0.07,-20.33 0,-42.76 0,-7.64 6.29,-11.18a17.67,17.67 0,0 1,7.48 -1.69q12.3,0 23.73,0a5.94,5.94 0,0 1,1.77 0.27c1.44,0.45 3,0.49 4.37,1.13q7.94,3.69 7.85,12.49 -0.25,23.72 0.25,43.09a10.52,10.52 0,0 0,3.45 7.89,8.43 8.43,0 0,0 13.05,-2.88 13.45,13.45 0,0 0,1 -5.33q-0.1,-20.39 0,-39.88c0,-6.48 2.17,-13.84 9,-15.7a24,24 0,0 1,6 -1.08q5.76,0 11.39,0.07a10.06,10.06 0,0 0,8.7 -3.85c2.38,-3.09 2.06,-7.7 2,-11.44q-0.24,-50.9 0,-100.2 0,-8.13 0.52,-16.54a0.48,0.48 0,0 0,-0.45 -0.51h-161a0.48,0.48 0,0 0,-0.49 0.48h0q0,59.14 -0.07,118.94c0,5.08 0.71,9.07 5.52,11.79a12.21,12.21 0,0 0,6.63 1.36c5.86,-0.24 12,-0.69 17.6,1.26"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M256.14,241.06c2.47,1.21 4.51,5.41 5.77,7.73a1.87,1.87 0,0 0,2.66 0.93l5.18,-2.23a1.7,1.7 0,0 0,1 -2.45,28.37 28.37,0 0,0 -8.27,-11.56c-9.1,-7.71 -17.26,1.13 -21.57,9.18a14.06,14.06 0,0 0,-1.4 3.83,1 1,0 0,0 0.29,0.94c1.51,1.49 4.46,2.26 6.39,3a2.41,2.41 0,0 0,3.13 -1.35l0.05,-0.14a19.08,19.08 0,0 1,4.66 -7.56,1.83 1.83,0 0,1 2.08,-0.34"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M244,262.69a0.45,0.45 0,0 1,0.2 0.6,0.4 0.4,0 0,1 -0.16,0.18c-8.73,5.77 -16.12,12.75 -23.42,20.14a77.72,77.72 0,0 0,-9.36 11.74c-5.51,8.31 -10.38,26.49 5.94,27.34 11,0.56 23.33,-4.52 33.21,-9.79 5.46,-2.91 10.75,-6.8 16,-10.29C279.23,294 293.2,281.16 301.47,267a24.68,24.68 0,0 0,2.5 -16.87c-0.84,-4.13 -6.55,-6.3 -10.17,-6.6a40.62,40.62 0,0 0,-15.52 2.07,98.9 98.9,0 0,0 -22.7,10.19 0.79,0.79 0,0 1,-0.84 0,136.77 136.77,0 0,0 -18.21,-8.14c-8.95,-3.25 -29.8,-8.43 -30.71,7.3 -0.2,3.48 1,6.81 2.12,10.08a8,8 0,0 1,0.25 4.07c-0.89,4.78 0.63,9.48 5.19,11.62q6.41,3 11.39,-1.84c6.58,-6.38 3.13,-16.56 -5.94,-17.65A3.7,3.7 0,0 1,215.6 257a2.54,2.54 0,0 1,2.53 -2.4c8,-0.67 18.37,4.28 25.87,8.09"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M295.13,307.57c0,0.63 0,1.25 0,1.88a2.06,2.06 0,0 1,-2.13 2.28c-8.24,0.66 -17.51,-3.69 -24.95,-7.42a2.45,2.45 0,0 0,-2.4 0.11l-6.94,4.3a1.72,1.72 0,0 0,-0.56 2.37,1.7 1.7,0 0,0 0.64,0.61 89.73,89.73 0,0 0,28.29 10.15c4.74,0.81 13.83,0.63 16.4,-4.11 3,-5.59 1.52,-10.36 0,-17 -0.66,-2.82 0.13,-5.78 -0.93,-8.44 -1.8,-4.51 -6.79,-8.14 -11.84,-6.81 -5.57,1.47 -10.46,7.66 -7.77,13.69q3.13,7 10.69,7a1.32,1.32 0,0 1,1.51 1.43"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M258.74,323c-4.24,6 -8,-2.7 -9.08,-5.69a1.92,1.92 0,0 0,-2.24 -1.23,21.29 21.29,0 0,0 -6.63,2.68 1.86,1.86 0,0 0,-0.8 2.55c3.17,7.55 12,19.16 21.61,12.47q5.94,-4.15 9.42,-12.37a2.53,2.53 0,0 0,-1.44 -3.69l-4.16,-1.88a2.34,2.34 0,0 0,-3.54 1.39,30.07 30.07,0 0,1 -3.14,5.77"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M252.39,295.58a1.36,1.36 0,0 1,-0.44 1.57,65 65,0 0,1 -6.55,4.29 2.69,2.69 0,0 1,-3.07 -0.08,124 124,0 0,1 -12.82,-10.07 0.93,0.93 0,0 0,-1.29 0c-4,4 -12.35,13 -12.18,18.91a2.06,2.06 0,0 0,1.92 2,26.58 26.58,0 0,0 11.29,-1.58 121.66,121.66 0,0 0,24.36 -12.43,212.63 212.63,0 0,0 25.07,-19.73c6,-5.5 14,-13.41 16,-21.4 1.65,-6.65 -13.58,-1.58 -16.06,-0.51q-6,2.61 -12.34,5.71a0.54,0.54 0,0 0,-0.25 0.71,0.64 0.64,0 0,0 0.19,0.21 137.63,137.63 0,0 1,12.46 9.29,2 2,0 0,1 0.3,3.15 21.39,21.39 0,0 1,-6 5.14,2.06 2.06,0 0,1 -2.34,-0.21 134.52,134.52 0,0 0,-14.92 -10.77,0.78 0.78,0 0,0 -0.9,0 157.9,157.9 0,0 0,-17.7 13.43,0.33 0.33,0 0,0 0,0.45l0.06,0.05q7.53,5.38 14.56,10.95a2.16,2.16 0,0 1,0.68 0.9"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M261.87,275a8.83,8.83 0,0 0,-12.49 0l-2,2a8.82,8.82 0,0 0,0 12.48h0l2,2a8.83,8.83 0,0 0,12.49 0l2,-2a8.82,8.82 0,0 0,0 -12.48h0l-2,-2"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FF000000"/>
<path
android:pathData="M304.6,123.77q25.48,14.78 37.52,40c5,10.5 7.2,21.79 8,33.18a1.4,1.4 0,0 1,-1.31 1.48h-0.08l-186.39,0.1a0.67,0.67 0,0 1,-0.67 -0.67c-0.09,-31.09 17.26,-57.72 43.91,-73.42a1.21,1.21 0,0 0,0.43 -1.64q-7.93,-13.45 -15.7,-27.43a12.35,12.35 0,0 1,-1.87 -7.43c0.68,-5.57 5.72,-5.35 9.95,-4.78a1.81,1.81 0,0 1,1 0.49c3.61,3.44 6.11,8.66 8.21,13.08 3.58,7.57 6.93,13.53 10.77,20a1.15,1.15 0,0 0,1.41 0.5,94.62 94.62,0 0,1 26,-6c15.29,-1.15 31.36,0.5 45.76,6a0.86,0.86 0,0 0,1.07 -0.39c3.87,-7.23 8.21,-14.11 11.54,-21.59a60.66,60.66 0,0 1,5.37 -9.64c2.28,-3.32 4.42,-2.57 7.93,-2.69a3.79,3.79 0,0 1,3.18 1.44,7.08 7.08,0 0,1 1.31,7.8q-3,6.9 -17.56,30.9A0.45,0.45 0,0 0,304.6 123.77ZM202.24,142.7a73.29,73.29 0,0 0,-26 41.57,0.6 0.6,0 0,0 0.46,0.71h0.12q78.3,0 156.93,0a3,3 0,0 0,1 -0.16,0.52 0.52,0 0,0 0.39,-0.7c-2.83,-12 -8.39,-23.5 -16.83,-32.58a93.87,93.87 0,0 0,-21.1 -17.25,63.89 63.89,0 0,0 -13.06,-5.64 98.42,98.42 0,0 0,-13.78 -3.44,89.93 89.93,0 0,0 -45.77,4.37 61,61 0,0 0,-13.44 6.6Q206.56,139.24 202.24,142.7Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M156,309.12a21.63,21.63 0,0 1,-19.68 21.15q-13,1.07 -20.5,-9.7a10.64,10.64 0,0 1,-1.8 -6q0,-49.69 0,-98.85a9.93,9.93 0,0 1,1.81 -5.86,22.21 22.21,0 0,1 35.45,-1.63c3.62,4.36 4.75,9.37 4.75,14.87Q156,265.18 156,309.12ZM142.95,222.12a8.63,8.63 0,0 0,-8.63 -8.63L134,213.49a8.63,8.63 0,0 0,-8.63 8.63h0L125.37,308.7a8.63,8.63 0,0 0,8.63 8.63h0.3A8.63,8.63 0,0 0,143 308.7Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M399,308.83a21.3,21.3 0,0 1,-21.27 21.34h-1.48A21.3,21.3 0,0 1,355 308.91h0l-0.16,-87.22a21.3,21.3 0,0 1,21.27 -21.34h1.48a21.3,21.3 0,0 1,21.33 21.26h0ZM385.48,221.45a7.87,7.87 0,0 0,-7.84 -7.9h-1a7.87,7.87 0,0 0,-7.9 7.84l-0.3,88a7.87,7.87 0,0 0,7.84 7.9h1a7.87,7.87 0,0 0,7.9 -7.84Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M242.54,363.52q-0.09,22.29 0.09,44.46c0.07,8.21 -4.76,15 -11.91,19.12 -4.3,2.49 -9.53,2.16 -14.42,1.79 -6.54,-0.5 -13,-7.88 -15.17,-13.15a22.1,22.1 0,0 1,-1.47 -8.58q0.12,-22.4 0.08,-43.79a0.42,0.42 0,0 0,-0.42 -0.41c-11.48,0.22 -22.67,1.25 -31.25,-7.75 -6.34,-6.66 -6.64,-12.59 -6.6,-22.33q0.27,-66.81 -0.08,-128a0.83,0.83 0,0 1,0.82 -0.83h187a0.8,0.8 0,0 1,0.8 0.81q0,64.92 0,129.87c0,16.14 -8.17,27.31 -25.11,28.21a115.4,115.4 0,0 1,-11.91 0c-0.77,0 -1.14,0.33 -1.13,1.09q0.19,21.76 0.07,41.42 -0.09,13 -10.08,20.67a15,15 0,0 1,-9.09 3c-3.37,0 -7.6,0.32 -10.62,-1.18q-9.9,-4.9 -12.66,-13.73 -1.54,-4.92 -1.41,-16 0.19,-17.55 0.06,-34.75a0.45,0.45 0,0 0,-0.52 -0.51l-24.62,0.05A0.48,0.48 0,0 0,242.54 363.52ZM204.46,350.63c5.26,1.85 7.52,8.11 7.51,13.3q0,18.68 0,40.92a18.92,18.92 0,0 0,0.66 5A8.41,8.41 0,0 0,225 415q5,-2.91 5,-10.15 -0.07,-20.33 0,-42.76 0,-7.64 6.29,-11.18a17.67,17.67 0,0 1,7.48 -1.69q12.3,0 23.73,0a5.94,5.94 0,0 1,1.77 0.27c1.44,0.45 3,0.49 4.37,1.13q7.94,3.69 7.85,12.49 -0.25,23.72 0.25,43.09a10.52,10.52 0,0 0,3.45 7.89,8.43 8.43,0 0,0 13.05,-2.88 13.45,13.45 0,0 0,1 -5.33q-0.1,-20.39 0,-39.88c0,-6.48 2.17,-13.84 9,-15.7a24,24 0,0 1,6 -1.08q5.76,0 11.39,0.07a10.06,10.06 0,0 0,8.7 -3.85c2.38,-3.09 2.06,-7.7 2,-11.44q-0.24,-50.9 0,-100.2 0,-8.13 0.52,-16.54a0.48,0.48 0,0 0,-0.45 -0.51h-161a0.48,0.48 0,0 0,-0.49 0.48h0q0,59.14 -0.07,118.94c0,5.08 0.71,9.07 5.52,11.79a12.21,12.21 0,0 0,6.63 1.36C192.72,349.13 198.89,348.68 204.46,350.63Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M256.14,241.06a1.83,1.83 0,0 0,-2.08 0.34A19.08,19.08 0,0 0,249.4 249a2.42,2.42 0,0 1,-3 1.54l-0.14,-0.05c-1.93,-0.76 -4.88,-1.53 -6.39,-3a1,1 0,0 1,-0.29 -0.94,14.06 14.06,0 0,1 1.4,-3.83c4.31,-8 12.47,-16.89 21.57,-9.18A28.37,28.37 0,0 1,270.78 245a1.7,1.7 0,0 1,-1 2.45l-5.18,2.23a1.87,1.87 0,0 1,-2.66 -0.93C260.65,246.47 258.61,242.27 256.14,241.06Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M244,262.69c-7.5,-3.81 -17.85,-8.76 -25.87,-8.09a2.54,2.54 0,0 0,-2.53 2.4,3.7 3.7,0 0,0 3.23,4.21c9.07,1.09 12.52,11.27 5.94,17.65q-5,4.83 -11.39,1.84c-4.56,-2.14 -6.08,-6.84 -5.19,-11.62a8,8 0,0 0,-0.25 -4.07c-1.1,-3.27 -2.32,-6.6 -2.12,-10.08 0.91,-15.73 21.76,-10.55 30.71,-7.3a136.77,136.77 0,0 1,18.21 8.14,0.79 0.79,0 0,0 0.84,0 98.9,98.9 0,0 1,22.7 -10.19,40.62 40.62,0 0,1 15.52,-2.07c3.62,0.3 9.33,2.47 10.17,6.6a24.68,24.68 0,0 1,-2.5 16.87c-8.27,14.21 -22.24,27.08 -35.09,35.66 -5.22,3.49 -10.51,7.38 -16,10.29 -9.88,5.27 -22.24,10.35 -33.21,9.79 -16.32,-0.85 -11.45,-19 -5.94,-27.34a77.72,77.72 0,0 1,9.36 -11.74c7.3,-7.39 14.69,-14.37 23.42,-20.14a0.45,0.45 0,0 0,0.14 -0.62A0.55,0.55 0,0 0,244 262.69ZM252.39,295.58a1.36,1.36 0,0 1,-0.44 1.57,65 65,0 0,1 -6.55,4.29 2.69,2.69 0,0 1,-3.07 -0.08,124 124,0 0,1 -12.82,-10.07 0.93,0.93 0,0 0,-1.29 0c-4,4 -12.35,13 -12.18,18.91a2.06,2.06 0,0 0,1.92 2,26.58 26.58,0 0,0 11.29,-1.58 121.66,121.66 0,0 0,24.36 -12.43,212.63 212.63,0 0,0 25.07,-19.73c6,-5.5 14,-13.41 16,-21.4 1.65,-6.65 -13.58,-1.58 -16.06,-0.51q-6,2.61 -12.34,5.71a0.54,0.54 0,0 0,-0.25 0.71,0.64 0.64,0 0,0 0.19,0.21 137.63,137.63 0,0 1,12.46 9.29,2 2,0 0,1 0.3,3.15 21.39,21.39 0,0 1,-6 5.14,2.06 2.06,0 0,1 -2.34,-0.21 134.52,134.52 0,0 0,-14.92 -10.77,0.78 0.78,0 0,0 -0.9,0 157.9,157.9 0,0 0,-17.7 13.43,0.33 0.33,0 0,0 0,0.45l0.06,0.05q7.53,5.38 14.56,10.95A2.16,2.16 0,0 1,252.39 295.58Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M247.42,276.94L249.39,274.98A8.83,8.83 90.2,0 1,261.88 275.02L263.88,277.04A8.83,8.83 90.2,0 1,263.83 289.53L261.86,291.49A8.83,8.83 90.2,0 1,249.37 291.44L247.37,289.43A8.83,8.83 90.2,0 1,247.42 276.94z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M293.62,306.14q-7.56,0 -10.69,-7c-2.69,-6 2.2,-12.22 7.77,-13.69 5.05,-1.33 10,2.3 11.84,6.81 1.06,2.66 0.27,5.62 0.93,8.44 1.56,6.67 3.08,11.44 0,17 -2.57,4.74 -11.66,4.92 -16.4,4.11a89.73,89.73 0,0 1,-28.29 -10.15,1.73 1.73,0 0,1 -0.69,-2.34 1.78,1.78 0,0 1,0.61 -0.64l6.94,-4.3a2.45,2.45 0,0 1,2.4 -0.11c7.44,3.73 16.71,8.08 24.95,7.42a2.06,2.06 0,0 0,2.13 -2.28c0,-0.63 0,-1.25 0,-1.88A1.32,1.32 0,0 0,293.62 306.14Z"
android:fillColor="#3ddc84"/>
<path
android:pathData="M249.66,317.34c1.05,3 4.84,11.72 9.08,5.69a30.07,30.07 0,0 0,3.14 -5.77,2.34 2.34,0 0,1 3.54,-1.39l4.16,1.88a2.53,2.53 0,0 1,1.44 3.69q-3.48,8.22 -9.42,12.37c-9.59,6.69 -18.44,-4.92 -21.61,-12.47a1.86,1.86 0,0 1,0.8 -2.55,21.29 21.29,0 0,1 6.63,-2.68A1.92,1.92 0,0 1,249.66 317.34Z"
android:fillColor="#3ddc84"/>
</group>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2c5.523,0 10,4.477 10,10a10,10 0,0 1,-19.995 0.324l-0.005,-0.324l0.004,-0.28c0.148,-5.393 4.566,-9.72 9.996,-9.72zM12,11h-1l-0.117,0.007a1,1 0,0 0,0 1.986l0.117,0.007v3l0.007,0.117a1,1 0,0 0,0.876 0.876l0.117,0.007h1l0.117,-0.007a1,1 0,0 0,0.876 -0.876l0.007,-0.117l-0.007,-0.117a1,1 0,0 0,-0.764 -0.857l-0.112,-0.02l-0.117,-0.006v-3l-0.007,-0.117a1,1 0,0 0,-0.876 -0.876l-0.117,-0.007zM12.01,8l-0.127,0.007a1,1 0,0 0,0 1.986l0.117,0.007l0.127,-0.007a1,1 0,0 0,0 -1.986l-0.117,-0.007z"
android:strokeLineJoin="round"
android:strokeWidth="0"
android:fillColor="#ffff"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,4 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/ic_launcher_background" />
<item android:drawable="@drawable/ic_launcher_foreground" />
</layer-list>

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