Repo cloned
This commit is contained in:
commit
11ea8025b0
214 changed files with 33943 additions and 0 deletions
73
app/src/main/AndroidManifest.xml
Normal file
73
app/src/main/AndroidManifest.xml
Normal 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>
|
||||
9
app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl
Normal file
9
app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl
Normal 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
2
app/src/main/assets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
kpimg
|
||||
*.kpm
|
||||
116
app/src/main/assets/InstallAP.sh
Normal file
116
app/src/main/assets/InstallAP.sh
Normal 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
|
||||
89
app/src/main/assets/UninstallAP.sh
Normal file
89
app/src/main/assets/UninstallAP.sh
Normal 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
|
||||
20
app/src/main/assets/boot_extract.sh
Normal file
20
app/src/main/assets/boot_extract.sh
Normal 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
|
||||
107
app/src/main/assets/boot_patch.sh
Normal file
107
app/src/main/assets/boot_patch.sh
Normal 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
|
||||
|
||||
74
app/src/main/assets/boot_unpatch.sh
Normal file
74
app/src/main/assets/boot_unpatch.sh
Normal 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
|
||||
537
app/src/main/assets/util_functions.sh
Normal file
537
app/src/main/assets/util_functions.sh
Normal 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
|
||||
}
|
||||
29
app/src/main/cpp/CMakeLists.txt
Normal file
29
app/src/main/cpp/CMakeLists.txt
Normal 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
294
app/src/main/cpp/apjni.cpp
Normal 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;
|
||||
}
|
||||
28
app/src/main/cpp/apjni.hpp
Normal file
28
app/src/main/cpp/apjni.hpp
Normal 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
|
||||
1334
app/src/main/cpp/jni_helper.hpp
Normal file
1334
app/src/main/cpp/jni_helper.hpp
Normal file
File diff suppressed because it is too large
Load diff
568
app/src/main/cpp/supercall.h
Normal file
568
app/src/main/cpp/supercall.h
Normal 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
|
||||
14
app/src/main/cpp/type_traits.hpp
Normal file
14
app/src/main/cpp/type_traits.hpp
Normal 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
|
||||
118
app/src/main/cpp/uapi/scdefs.h
Normal file
118
app/src/main/cpp/uapi/scdefs.h
Normal 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
3
app/src/main/cpp/version
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#define MAJOR 0
|
||||
#define MINOR 12
|
||||
#define PATCH 0
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
308
app/src/main/java/me/bmax/apatch/APatchApp.kt
Normal file
308
app/src/main/java/me/bmax/apatch/APatchApp.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
155
app/src/main/java/me/bmax/apatch/Natives.kt
Normal file
155
app/src/main/java/me/bmax/apatch/Natives.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
72
app/src/main/java/me/bmax/apatch/services/RootServices.java
Normal file
72
app/src/main/java/me/bmax/apatch/services/RootServices.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/me/bmax/apatch/ui/CrashHandleActivity.kt
Normal file
151
app/src/main/java/me/bmax/apatch/ui/CrashHandleActivity.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
209
app/src/main/java/me/bmax/apatch/ui/MainActivity.kt
Normal file
209
app/src/main/java/me/bmax/apatch/ui/MainActivity.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
app/src/main/java/me/bmax/apatch/ui/WebUIActivity.kt
Normal file
89
app/src/main/java/me/bmax/apatch/ui/WebUIActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
533
app/src/main/java/me/bmax/apatch/ui/component/Dialog.kt
Normal file
533
app/src/main/java/me/bmax/apatch/ui/component/Dialog.kt
Normal 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())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
169
app/src/main/java/me/bmax/apatch/ui/component/SearchBar.kt
Normal file
169
app/src/main/java/me/bmax/apatch/ui/component/SearchBar.kt
Normal 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 = "" }
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
604
app/src/main/java/me/bmax/apatch/ui/screen/APM.kt
Normal file
604
app/src/main/java/me/bmax/apatch/ui/screen/APM.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
app/src/main/java/me/bmax/apatch/ui/screen/AboutScreen.kt
Normal file
193
app/src/main/java/me/bmax/apatch/ui/screen/AboutScreen.kt
Normal 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) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
152
app/src/main/java/me/bmax/apatch/ui/screen/ExecuteAPMAction.kt
Normal file
152
app/src/main/java/me/bmax/apatch/ui/screen/ExecuteAPMAction.kt
Normal 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("[H[J")) { // 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
996
app/src/main/java/me/bmax/apatch/ui/screen/Home.kt
Normal file
996
app/src/main/java/me/bmax/apatch/ui/screen/Home.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
app/src/main/java/me/bmax/apatch/ui/screen/Install.kt
Normal file
175
app/src/main/java/me/bmax/apatch/ui/screen/Install.kt
Normal 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("[H[J")) { // clear command
|
||||
text = tempText.substring(6)
|
||||
} else {
|
||||
text += tempText
|
||||
}
|
||||
logContent.append(it).append("\n")
|
||||
}, onStderr = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // 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)
|
||||
}
|
||||
197
app/src/main/java/me/bmax/apatch/ui/screen/InstallModeSelect.kt
Normal file
197
app/src/main/java/me/bmax/apatch/ui/screen/InstallModeSelect.kt
Normal 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) }
|
||||
},
|
||||
)
|
||||
}
|
||||
607
app/src/main/java/me/bmax/apatch/ui/screen/KPM.kt
Normal file
607
app/src/main/java/me/bmax/apatch/ui/screen/KPM.kt
Normal 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) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
624
app/src/main/java/me/bmax/apatch/ui/screen/Patches.kt
Normal file
624
app/src/main/java/me/bmax/apatch/ui/screen/Patches.kt
Normal 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)) })
|
||||
}
|
||||
751
app/src/main/java/me/bmax/apatch/ui/screen/Settings.kt
Normal file
751
app/src/main/java/me/bmax/apatch/ui/screen/Settings.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
267
app/src/main/java/me/bmax/apatch/ui/screen/SuperUser.kt
Normal file
267
app/src/main/java/me/bmax/apatch/ui/screen/SuperUser.kt
Normal 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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/AmberTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/AmberTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/BlueGreyTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/BlueGreyTheme.kt
Normal 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,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/BlueTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/BlueTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/BrownTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/BrownTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/CyanTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/CyanTheme.kt
Normal 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,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/DeepOrangeTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/DeepOrangeTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/DeepPurpleTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/DeepPurpleTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/GreenTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/GreenTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/IndigoTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/IndigoTheme.kt
Normal 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,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/LightBlueTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/LightBlueTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/LightGreenTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/LightGreenTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/LimeTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/LimeTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/OrangeTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/OrangeTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/PinkTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/PinkTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/PurpleTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/PurpleTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/RedTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/RedTheme.kt
Normal 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,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/SakuraTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/SakuraTheme.kt
Normal 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,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/TealTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/TealTheme.kt
Normal 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,
|
||||
)
|
||||
172
app/src/main/java/me/bmax/apatch/ui/theme/Theme.kt
Normal file
172
app/src/main/java/me/bmax/apatch/ui/theme/Theme.kt
Normal 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
|
||||
)
|
||||
}
|
||||
33
app/src/main/java/me/bmax/apatch/ui/theme/Type.kt
Normal file
33
app/src/main/java/me/bmax/apatch/ui/theme/Type.kt
Normal 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
|
||||
)
|
||||
*/
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/YellowTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/YellowTheme.kt
Normal 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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/me/bmax/apatch/ui/viewmodel/KPModel.kt
Normal file
66
app/src/main/java/me/bmax/apatch/ui/viewmodel/KPModel.kt
Normal 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
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
139
app/src/main/java/me/bmax/apatch/ui/webui/MimeUtil.java
Normal file
139
app/src/main/java/me/bmax/apatch/ui/webui/MimeUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
app/src/main/java/me/bmax/apatch/ui/webui/SuFilePathHandler.java
Normal file
195
app/src/main/java/me/bmax/apatch/ui/webui/SuFilePathHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
175
app/src/main/java/me/bmax/apatch/ui/webui/WebViewInterface.kt
Normal file
175
app/src/main/java/me/bmax/apatch/ui/webui/WebViewInterface.kt
Normal 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())
|
||||
391
app/src/main/java/me/bmax/apatch/util/APatchCli.kt
Normal file
391
app/src/main/java/me/bmax/apatch/util/APatchCli.kt
Normal 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
|
||||
)
|
||||
}
|
||||
149
app/src/main/java/me/bmax/apatch/util/APatchKeyHelper.java
Normal file
149
app/src/main/java/me/bmax/apatch/util/APatchKeyHelper.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
52
app/src/main/java/me/bmax/apatch/util/DeviceInfoUtils.kt
Normal file
52
app/src/main/java/me/bmax/apatch/util/DeviceInfoUtils.kt
Normal 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")
|
||||
}
|
||||
128
app/src/main/java/me/bmax/apatch/util/Downloader.kt
Normal file
128
app/src/main/java/me/bmax/apatch/util/Downloader.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
569
app/src/main/java/me/bmax/apatch/util/HanziToPinyin.java
Normal file
569
app/src/main/java/me/bmax/apatch/util/HanziToPinyin.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/me/bmax/apatch/util/IOStreamUtils.kt
Normal file
36
app/src/main/java/me/bmax/apatch/util/IOStreamUtils.kt
Normal 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) }
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package me.bmax.apatch.util
|
||||
|
||||
data class LatestVersionInfo(
|
||||
val versionCode: Int = 0, val downloadUrl: String = "", val changelog: String = ""
|
||||
)
|
||||
94
app/src/main/java/me/bmax/apatch/util/LogEvent.kt
Normal file
94
app/src/main/java/me/bmax/apatch/util/LogEvent.kt
Normal 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
|
||||
}
|
||||
91
app/src/main/java/me/bmax/apatch/util/PkgConfig.kt
Normal file
91
app/src/main/java/me/bmax/apatch/util/PkgConfig.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
app/src/main/java/me/bmax/apatch/util/Version.kt
Normal file
153
app/src/main/java/me/bmax/apatch/util/Version.kt
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
87
app/src/main/java/me/bmax/apatch/util/ui/HyperlinkText.kt
Normal file
87
app/src/main/java/me/bmax/apatch/util/ui/HyperlinkText.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
36
app/src/main/res/drawable/device_mobile_down.xml
Normal file
36
app/src/main/res/drawable/device_mobile_down.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/github.xml
Normal file
10
app/src/main/res/drawable/github.xml
Normal 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>
|
||||
17
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
17
app/src/main/res/drawable/ic_launcher_background.xml
Normal 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>
|
||||
108
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
108
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal 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>
|
||||
108
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
108
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal 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>
|
||||
12
app/src/main/res/drawable/info_circle_filled.xml
Normal file
12
app/src/main/res/drawable/info_circle_filled.xml
Normal 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>
|
||||
4
app/src/main/res/drawable/launcher_splash.xml
Normal file
4
app/src/main/res/drawable/launcher_splash.xml
Normal 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>
|
||||
42
app/src/main/res/drawable/package_import.xml
Normal file
42
app/src/main/res/drawable/package_import.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<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,21l-8,-4.5v-9l8,-4.5l8,4.5v4.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,12l8,-4.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,12v9"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,12l-8,-4.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M22,18h-7"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M18,15l-3,3l3,3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/settings.xml
Normal file
18
app/src/main/res/drawable/settings.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<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="M10.325,4.317c0.426,-1.756 2.924,-1.756 3.35,0a1.724,1.724 0,0 0,2.573 1.066c1.543,-0.94 3.31,0.826 2.37,2.37a1.724,1.724 0,0 0,1.065 2.572c1.756,0.426 1.756,2.924 0,3.35a1.724,1.724 0,0 0,-1.066 2.573c0.94,1.543 -0.826,3.31 -2.37,2.37a1.724,1.724 0,0 0,-2.572 1.065c-0.426,1.756 -2.924,1.756 -3.35,0a1.724,1.724 0,0 0,-2.573 -1.066c-1.543,0.94 -3.31,-0.826 -2.37,-2.37a1.724,1.724 0,0 0,-1.065 -2.572c-1.756,-0.426 -1.756,-2.924 0,-3.35a1.724,1.724 0,0 0,1.066 -2.573c-0.94,-1.543 0.826,-3.31 2.37,-2.37c1,0.608 2.296,0.07 2.572,-1.065z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9,12a3,3 0,1 0,6 0a3,3 0,0 0,-6 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/telegram.xml
Normal file
9
app/src/main/res/drawable/telegram.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#ffff"
|
||||
android:pathData="M769.45,245.76c21.08,-7.47 42.28,-15.36 65.19,-15.36 27.05,0.04 39.94,11.73 40.32,38.83 0.68,44.03 -8.66,87.17 -13.99,130.69 -2.99,24.32 -45.27,285.95 -64.77,392.19 -4.05,22.06 -7.04,44.37 -16.38,65.15 -15.15,33.66 -36.1,43.14 -71.21,31.7 -15.96,-5.21 -29.99,-13.99 -43.78,-23.34 -66.99,-45.27 -211.67,-142.98 -216.7,-146.99 -31.15,-24.87 -36.95,-45.4 -5.42,-78.59 34.86,-36.69 132.44,-124.37 143.15,-134.83a956.84,956.84 0,0 0,84.91 -83.03c5.85,-6.4 12.76,-13.4 5.21,-22.27 -7.34,-8.62 -15.79,-3.84 -23.17,0.73a2884.91,2884.91 0,0 0,-135.42 89.3c-53.72,35.71 -108.46,70.02 -160.77,107.65 -42.67,30.68 -86.74,41.26 -137.47,26.07 -37.89,-11.31 -76.12,-21.5 -113.15,-35.41 -13.87,-5.16 -30.29,-10.45 -30.98,-28.5 -0.6,-16.85 14.12,-24.45 26.79,-31.15 44.16,-23.34 310.61,-136.83 383.62,-166.91 94.55,-38.95 187.61,-81.71 284.03,-115.93z" />
|
||||
</vector>
|
||||
36
app/src/main/res/drawable/trash.xml
Normal file
36
app/src/main/res/drawable/trash.xml
Normal 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="M4,7l16,0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M10,11l0,6"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14,11l0,6"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M5,7l1,12a2,2 0,0 0,2 2h8a2,2 0,0 0,2 -2l1,-12"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9,7v-3a1,1 0,0 1,1 -1h4a1,1 0,0 1,1 1v3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
55
app/src/main/res/drawable/weblate.xml
Normal file
55
app/src/main/res/drawable/weblate.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="100dp"
|
||||
android:height="74dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="74">
|
||||
<path
|
||||
android:pathData="M63.62,55.8a10.73,10.73 0,0 1,-4.24 -0.89c-3.06,-1.33 -5.75,-3.85 -7.94,-7.25a36.01,36.01 0,0 0,1.69 -3.43c2.2,-5.12 3.25,-10.73 3.33,-16.3a7.55,7.55 0,0 1,-0.03 -0.33l-0.01,-0.29c-0,-2.18 -0.34,-4.39 -1.09,-6.45 -0.87,-2.37 -2.24,-4.75 -4.43,-5.67a2.97,2.97 0,0 0,-1.23 -0.23c-2.75,-5.14 -2.84,-10.07 0,-14.77l0.15,0c3.35,0.04 6.67,1.07 9.51,2.88 7.74,4.92 11.59,14.5 11.68,23.91 0,0.11 0,0.22 -0,0.33h0.04c-0.01,9.94 -2.4,20.03 -7.44,28.49zM46.47,71.41c-7.22,2.97 -15.61,2.81 -22.77,-0.51 -8.24,-3.82 -14.53,-11.26 -18.41,-19.74 -6.63,-14.51 -6.78,-31.86 -0.49,-46.59 4.72,1.89 8.92,-1.12 8.92,-1.12s-0.01,4.63 4.47,6.95c-4.6,10.78 -4.49,23.58 0.12,34.09 2.22,5.05 5.61,9.76 10.31,12.42 1.68,0.95 3.52,1.56 5.42,1.79l0.02,0.03c3.35,5.22 7.57,9.58 12.41,12.68z"
|
||||
android:fillType="nonZero">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="-1.49"
|
||||
android:startY="28.42"
|
||||
android:endX="48.03"
|
||||
android:endY="28.42"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF00D2E6"/>
|
||||
<item android:offset="1" android:color="#FF2ECCAA"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M63.62,55.81a10.73,10.73 0,0 1,-4.24 -0.89c-3.06,-1.33 -5.75,-3.85 -7.94,-7.25a36,36 0,0 0,1.69 -3.43,38.89 38.89,0 0,0 1.68,-4.75c1.06,-3.75 15.19,1.01 13.04,7.18a50.18,50.18 0,0 1,-4.23 9.14zM46.47,71.41c-7.22,2.97 -15.61,2.81 -22.77,-0.51 -8.24,-3.82 0.22,-16.65 4.92,-13.99a14.57,14.57 0,0 0,5.41 1.79l0.02,0.04c3.35,5.22 7.56,9.58 12.41,12.68z"
|
||||
android:strokeAlpha="0.7"
|
||||
android:fillType="evenOdd"
|
||||
android:fillAlpha="0.7">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="24.88"
|
||||
android:startY="58.82"
|
||||
android:endX="58.55"
|
||||
android:endY="47.58"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#00000000"/>
|
||||
<item android:offset="0.51" android:color="#FF000000"/>
|
||||
<item android:offset="1" android:color="#00000000"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M28.38,27.31a8.15,8.15 0,0 1,-0 -0.33c0.09,-9.41 3.94,-18.99 11.68,-23.91 2.84,-1.81 6.16,-2.84 9.51,-2.88h0.15v14.77a2.97,2.97 0,0 0,-1.23 0.23c-2.19,0.93 -3.57,3.3 -4.43,5.67 -0.75,2.06 -1.08,4.27 -1.09,6.45l-0.01,0.28a7.55,7.55 0,0 1,-0.03 0.33c0.08,5.57 1.13,11.18 3.33,16.3 2.47,5.74 6.39,11.01 11.93,13.42 4.18,1.82 8.81,1.39 12.59,-0.75 4.7,-2.66 8.09,-7.37 10.31,-12.42 4.61,-10.5 4.72,-23.31 0.12,-34.09C85.69,8.08 85.68,3.45 85.68,3.45s4.2,3.01 8.92,1.12c6.29,14.73 6.14,32.08 -0.49,46.59 -3.88,8.48 -10.17,15.92 -18.41,19.74 -7.33,3.4 -15.95,3.49 -23.3,0.29 -6.59,-2.87 -11.96,-8.05 -15.82,-14.07 -5.59,-8.72 -8.24,-19.34 -8.25,-29.8h0.04z"
|
||||
android:fillType="nonZero">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="100.89"
|
||||
android:startY="29.66"
|
||||
android:endX="51.44"
|
||||
android:endY="29.66"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF1FA385"/>
|
||||
<item android:offset="1" android:color="#FF2ECCAA"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
unqualifiedResLocale=en
|
||||
6
app/src/main/res/values-ab/strings.xml
Normal file
6
app/src/main/res/values-ab/strings.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="su_pkg_excluded_setting_summary">\t</string>
|
||||
<string name="about_app_version">%1$s</string>
|
||||
<string name="about_app_desc">…\t\t…“”” </string>
|
||||
</resources>
|
||||
3
app/src/main/res/values-apc/strings.xml
Normal file
3
app/src/main/res/values-apc/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue