55 Commits
v3.0 ... v3.1

Author SHA1 Message Date
ShirkNeko
bb2d8fd7e0 Refactoring the KsuIsValid Check Logic
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
2025-05-12 00:06:17 +08:00
ShirkNeko
2b6d418fe6 [skip ci]:Removing unused LocalDensity variables 2025-05-11 23:36:09 +08:00
ShirkNeko
d8b1126b96 [skip ci]:Modify comments 2025-05-11 23:32:46 +08:00
ShirkNeko
2eeddcfa80 Modify UI layout
- Adjust the maximum number of SwitchItem rows to optimize the layout and spacing of interface buttons
2025-05-11 23:30:48 +08:00
ShirkNeko
59e3675a36 {docs}:Fixed description of KPROBES and manual hooks, simplified content 2025-05-10 12:17:00 +08:00
米凛MiRin
bc386f080d 修正 README 中错误和误导性内容。 (#71)
* 修正文档

* Update README-en.md

* Update README-ja.md
2025-05-09 22:27:53 +08:00
ShirkNeko
2dc1377154 Update Android Gradle plugin version to 8.10.0 2025-05-09 19:14:46 +08:00
WenHao2130
610852e2f2 [skip ci]: manager: modify background image control logic (#70)
* manager: modify background image control logic

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

* manager: modify padding

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

* docs: update README.md README-en.md README-ja.md

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>

---------

Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-05-09 16:58:01 +08:00
ShirkNeko
15b19bb8ce Remove unnecessary card color calculations and simplify theme colors 2025-05-08 11:58:28 +08:00
ShirkNeko
4a598b1837 [skip ci]: Correction of translation errors 2025-05-07 11:30:01 +08:00
ShirkNeko
caee2417d6 [skip ci]:
Fixing tools used by kernels under 5.10
-Add Slot selection is not displayed for non-ab partitions
2025-05-05 22:09:01 +08:00
ShirkNeko
349ca36d4e [skip ci]: Remove unnecessary center point calculation code to simplify bitmap transformation logic 2025-05-05 21:09:31 +08:00
ShirkNeko
ec86f5caf2 [skip ci]:Simplifying Conditional Judgment in the Selection of Installation Methods 2025-05-05 21:09:31 +08:00
ShirkNeko
b5a5cdfcd2 [skip ci]: Fixed “Kernel Module” to “KPM” in string resources. 2025-05-05 21:09:31 +08:00
YC酱luyancib
72d799e065 [skip]: manager: adjust translate on zh-rCN 2025-05-05 21:09:30 +08:00
ShirkNeko
d06f22dcd0 manager: continue to improve the UI
- Expose anykernel3 flashing as long as there is root.
- Opt some styles
2025-05-05 21:09:30 +08:00
ShirkNeko
cb90630f27 Optimize the interface, add hidden link card function, adjust scrolling behavior, clean up unnecessary code 2025-05-05 21:09:30 +08:00
Re*Index. (ot_inc)
59ad9204d0 Update Japanese translated (#64) 2025-05-05 21:09:30 +08:00
ShirkNeko
cb97c16f5e Fix LKM build error due to kernel module listing
Co-authored-by: James McConnell <bins4us@hotmail.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-05 21:04:25 +08:00
ShirkNeko
69b48d5345 Comment out the cleanup command to avoid accidentally deleting protected exports. 2025-04-30 20:27:54 +08:00
ShirkNeko
45ed4708c9 Optimize the HomeScreen component, refactor the device model acquisition logic, add anti-shake scrolling processing, clean up unused imports 2025-04-30 20:01:39 +08:00
ShirkNeko
f3c77bdb3b [skip ci]: Remove unused animation imports to optimize code cleanliness 2025-04-30 19:49:59 +08:00
ShirkNeko
dc0eb9eec1 Fix duplicate creation of popup windows 2025-04-30 19:48:40 +08:00
ShirkNeko
83dd6443cb Optimize KpmScreen interface layout, adjust button and text display, update signature configuration code 2025-04-30 02:49:09 +08:00
ShirkNeko
3d77f2d135 Adjust the spacing and size of interface elements to optimize the layout effect 2025-04-29 21:46:38 +08:00
ShirkNeko
1ea219bddc Updated GKI installation selection style 2025-04-29 18:07:29 +08:00
ShirkNeko
39adba62d1 Update the default theme color to blue and remove the related blue theme code 2025-04-29 17:29:45 +08:00
ShirkNeko
3526e84e04 Refactor the UI to rewrite the interface (#61) 2025-04-29 15:52:56 +08:00
ShirkNeko
bfdb706b60 Add kernel version and patch tool version log information
- Should fix the 5.10 bug where you can't swipe write

Signed-off-by: ShirkNeko 109797057+ShirkNeko@users.noreply.github.com
2025-04-28 16:05:59 +08:00
ShirkNeko
a297e07055 Adjust the prompt for file selection and add instructions for mirror repair.
- Modify the maximum height of the progress bar to improve user experience
- Add localized strings for error messages and installation methods.
-Optimize the installation interface

Signed-off-by: ShirkNeko 109797057+ShirkNeko@users.noreply.github.com
2025-04-28 14:39:09 +08:00
ShirkNeko
56b4664ec7 Optimized the UI of the slot selection dialog box, added separator lines and button styles, and improved the display logic of the current slot. 2025-04-27 22:37:56 +08:00
ShirkNeko
70f7c75a92 Add custom color and transparency settings to the top app bar 2025-04-27 20:32:17 +08:00
ShirkNeko
e414b4de92 Adding a localized message for a failed swipe 2025-04-27 20:27:29 +08:00
ShirkNeko
79e68f473f Update Chinese and English strings, change Anykernel3 related descriptions to generic Kernel descriptions. 2025-04-27 20:21:17 +08:00
ShirkNeko
6656604809 Add the function of obtaining and restoring the original slot, and display the current slot information in the slot selection dialog box
-It should be possible to fix the issue of selecting slot positions

Signed-off-by: ShirkNeko 109797057+ShirkNeko@users.noreply.github.com
2025-04-27 19:35:55 +08:00
ShirkNeko
85b4d11912 Improve the ui and function of the anykernel3 flashing interface.
- Add self-selected brushwrite A/B slot (not perfect)

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-27 18:01:45 +08:00
ShirkNeko
7769a23f59 Opt the image editing dialog box, add full-screen zoom function, improve the panning limit to ensure the security of image transformation. 2025-04-27 14:02:16 +08:00
ShirkNeko
c442f43090 Add free adjustment of image position when selecting background
- Updated AGP version to 8.9.2.
- Added support for Android 16 (36).
- Replaced the new API and fixed some minor bugs.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-27 04:05:49 +08:00
WenHao2130
d73670bf43 [skip ci]: manager: update simplified chinese translation (#59)
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-26 15:42:08 +08:00
ShirkNeko
dd1967f0d0 Update the README file to include a thank you message to DARKWWEE. 2025-04-26 15:41:23 +08:00
ShirkNeko
e3d2fc64ac Update the minimum supported kernel version and standardize kernel-related constants to 12800. 2025-04-25 14:03:16 +08:00
ShirkNeko
e07f20bf29 [skip ci]: Update kpm module display logic, fix installed and uninstalled name display 2025-04-25 14:00:18 +08:00
ShirkNeko
34f216181f Expose the getSuSFSDaemonPath method to support the installation of the SuSFSD daemon. 2025-04-23 23:06:19 +08:00
ShirkNeko
8aef775474 Opt InfoCard component, add icon support, improve information presentation 2025-04-23 22:23:02 +08:00
ShirkNeko
f669ad92b6 Refactor Kpm.kt, optimize file type checking logic, add ELF file detection, simplify string command execution 2025-04-23 21:06:21 +08:00
ShirkNeko
cc0b272770 Modified Kpm file type validation and check module ID extraction logic
Fix the problem that the specified kpm module could not be deleted after uninstallation due to a mismatch between the file type and the actual module name.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-23 03:33:46 +08:00
ShirkNeko
9ea6de340d Refactor the namespace to com.sukisu.ultra, add IKsuInterface and LatestVersionInfo data classes, and remove obsolete classes and methods. 2025-04-23 02:24:55 +08:00
Qumolama.d
be37f8a2a3 Update English translation and remove unused keys (#56)
* Remove untranslatable keys

* Fix English translations

* Remove unused string entry

---------

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-22 13:39:28 +08:00
Re*Index. (ot_inc)
8a12fac39f [skip ci]: fix typo & update README (#55)
* Update strings.xml

* fix typo

* Update README-en.md

* Update README-ja.md
2025-04-22 13:37:17 +08:00
Re*Index. (ot_inc)
0242fe12e3 Update Japanese & fix text(#54) 2025-04-21 20:15:08 +08:00
ShirkNeko
acf2e1a5ec Update KSU_GIT_VERSION to use the master branch count and change the KernelSU manager name to SukiSU 2025-04-21 17:33:29 +08:00
ShirkNeko
626db4be56 [skip ci]: Adding a check for LKM mode to the KPM info card 2025-04-21 09:20:38 +08:00
ShirkNeko
5941fa1ec7 manager: hide root-related features if kernelsu version null (#71)
Related PR:
tiann#2483

Also attempting to address this:
tiann#2483 (comment)

Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-20 22:30:23 +08:00
ShirkNeko
1dd8651a1a Remove unused functions and data classes and optimize code structure; update string resources to support new features 2025-04-20 22:01:35 +08:00
ShirkNeko
33dd0ca16b Add check for GKI version and KERNEL_TYPE setting 2025-04-19 21:44:41 +08:00
101 changed files with 9229 additions and 5289 deletions

137
.github/workflows/avd-kernel.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: GKI Kernel Build
on:
workflow_call:
inputs:
version_name:
required: true
type: string
description: >
With SUBLEVEL of kernel,
for example: android12-5.10.66
arch:
required: true
type: string
description: >
Build arch: aarch64/x86_64
debug:
required: false
type: boolean
default: true
manifest_name:
required: false
type: string
description: >
Local repo manifest xml path,
typically for AVD kernel build.
secrets:
BOOT_SIGN_KEY:
required: false
CHAT_ID:
required: false
BOT_TOKEN:
required: false
MESSAGE_THREAD_ID:
required: false
jobs:
build:
name: Build ${{ inputs.version_name }}
runs-on: ubuntu-22.04
steps:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 8192
temp-reserve-mb: 2048
remove-dotnet: 'true'
remove-android: 'true'
remove-haskell: 'true'
remove-codeql: 'true'
- uses: actions/checkout@v4
with:
path: KernelSU
fetch-depth: 0
- name: Setup need_upload
id: need_upload
run: |
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
echo "UPLOAD=true" >> $GITHUB_OUTPUT
else
echo "UPLOAD=false" >> $GITHUB_OUTPUT
fi
- name: Setup kernel source
run: |
echo "Free space:"
df -h
cd $GITHUB_WORKSPACE
sudo apt-get install repo -y
mkdir android-kernel && cd android-kernel
repo init --depth=1 -u https://android.googlesource.com/kernel/manifest -m "$GITHUB_WORKSPACE/KernelSU/.github/manifests/${{ inputs.manifest_name }}" --repo-rev=v2.16
repo --version
repo --trace sync -c -j$(nproc --all) --no-tags
df -h
- name: Setup KernelSU
env:
PATCH_PATH: ${{ inputs.patch_path }}
IS_DEBUG_KERNEL: ${{ inputs.debug }}
run: |
cd $GITHUB_WORKSPACE/android-kernel
echo "[+] KernelSU setup"
GKI_ROOT=$(pwd)
echo "[+] GKI_ROOT: $GKI_ROOT"
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
echo "[+] Add KernelSU driver to Makefile"
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
echo "[+] Apply KernelSU patches"
cd $GKI_ROOT/common/ && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/$PATCH_PATH/*.patch || echo "[-] No patch found"
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
echo "[+] Enable debug features for kernel"
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
fi
repo status
echo "[+] KernelSU setup done."
cd $GITHUB_WORKSPACE/KernelSU
VERSION=$(($(git rev-list --count HEAD) + 10200))
echo "VERSION: $VERSION"
echo "kernelsu_version=$VERSION" >> $GITHUB_ENV
- name: Make working directory clean to avoid dirty
working-directory: android-kernel
run: |
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
git config --global user.email "bot@kernelsu.org"
git config --global user.name "KernelSUBot"
cd common/ && git add -A && git commit -a -m "Add KernelSU"
repo status
- name: Build kernel
working-directory: android-kernel
run: |
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
fi
tools/bazel run --config=fast --config=stamp --lto=thin //common-modules/virtual-device:virtual_device_${{ inputs.arch }}_dist -- --dist_dir=dist
NAME=kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
TARGET_IMAGE=dist/bzImage
if [ ! -e $TARGET_IMAGE ]; then
TARGET_IMAGE=dist/Image
fi
mv $TARGET_IMAGE $NAME
echo "file_path=android-kernel/$NAME" >> $GITHUB_ENV
- name: Upload Kernel
uses: actions/upload-artifact@v4
with:
name: kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
path: "${{ env.file_path }}"

74
.github/workflows/build-lkm.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Build LKM for KernelSU
on:
workflow_call:
inputs:
upload:
required: true
type: boolean
default: true
description: "Whether to upload to branch"
secrets:
# username:github_pat
TOKEN:
required: true
workflow_dispatch:
inputs:
upload:
required: true
type: boolean
default: true
description: "Whether to upload to branch"
jobs:
build-lkm:
strategy:
matrix:
include:
- version: "android12-5.10"
sub_level: 233
os_patch_level: 2025-02
- version: "android13-5.10"
sub_level: 234
os_patch_level: 2025-03
- version: "android13-5.15"
sub_level: 178
os_patch_level: 2025-03
- version: "android14-5.15"
sub_level: 178
os_patch_level: 2025-03
- version: "android14-6.1"
sub_level: 129
os_patch_level: 2025-04
- version: "android15-6.6"
sub_level: 82
os_patch_level: 2025-04
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
uses: ./.github/workflows/gki-kernel.yml
with:
version: ${{ matrix.version }}
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
os_patch_level: ${{ matrix.os_patch_level }}
build_lkm: true
push-to-branch:
needs: [build-lkm]
runs-on: ubuntu-latest
if: ${{ inputs.upload }}
steps:
- name: Download all workflow run artifacts
uses: actions/download-artifact@v4
with:
path: bin/
merge-multiple: true
- name: Push to branch LKM
run: |
cd bin
git config --global init.defaultBranch lkm
git init
git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }}
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
find . -type f
git add .
git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git push --force --set-upstream origin lkm

View File

@@ -16,8 +16,76 @@ on:
- 'manager/**'
workflow_call:
workflow_dispatch:
inputs:
build_lkm:
required: true
type: choice
default: "auto"
options:
- "true"
- "false"
- "auto"
description: "Whether to build lkm"
upload_lkm:
required: true
type: boolean
default: true
description: "Whether to upload lkm"
jobs:
check-build-lkm:
runs-on: ubuntu-latest
outputs:
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
steps:
- name: check build
id: check-build
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
kernel_changed="${{ inputs.build_lkm }}"
else
kernel_changed=true
mkdir tmp
cd tmp
git config --global init.defaultBranch bot
git config --global user.name 'Bot'
git config --global user.email 'bot@github.shirkneko.io'
git init .
git remote add origin https://github.com/${{ github.repository }}
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
git fetch origin $CURRENT_COMMIT --depth=1
git fetch origin lkm --depth=1
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
echo "LKM_COMMIT=$LKM_COMMIT"
git fetch origin "$LKM_COMMIT" --depth=1
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
cd ..
rm -rf tmp
fi
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
need_upload=true
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
need_upload="${{ inputs.upload_lkm }}"
else
need_upload=false
fi
echo "kernel changed: $kernel_changed"
echo "need upload: $need_upload"
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
build-lkm:
needs: check-build-lkm
uses: ./.github/workflows/build-lkm.yml
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
with:
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
secrets: inherit
build-susfs:
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
@@ -29,7 +97,8 @@ jobs:
os: ${{ matrix.os }}
build-kpmmgr:
needs: build-susfs
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
@@ -41,7 +110,8 @@ jobs:
os: ${{ matrix.os }}
build-ksud:
needs: build-kpmmgr
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
@@ -53,6 +123,8 @@ jobs:
with:
target: ${{ matrix.target }}
os: ${{ matrix.os }}
pack_lkm: true
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
build-manager:
if: ${{ always() }}

79
.github/workflows/gki-kernel-mock.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: GKI Kernel Build
on:
workflow_call:
inputs:
version:
required: true
type: string
description: >
Output directory of gki,
for example: android12-5.10
version_name:
required: true
type: string
description: >
With SUBLEVEL of kernel,
for example: android12-5.10.66
tag:
required: true
type: string
description: >
Part of branch name of common kernel manifest,
for example: android12-5.10-2021-11
os_patch_level:
required: false
type: string
description: >
Patch level of common kernel manifest,
for example: 2021-11
default: 2022-05
patch_path:
required: false
type: string
description: >
Directory name of .github/patches/<patch_path>
for example: 5.10
use_cache:
required: false
type: boolean
default: true
embed_ksud:
required: false
type: string
default: ksud-aarch64-linux-android
description: >
Artifact name of prebuilt ksud to be embedded
for example: ksud-aarch64-linux-android
debug:
required: false
type: boolean
default: false
build_lkm:
required: false
type: boolean
default: false
secrets:
BOOT_SIGN_KEY:
required: false
CHAT_ID:
required: false
BOT_TOKEN:
required: false
MESSAGE_THREAD_ID:
required: false
jobs:
mock_build:
name: Mock build ${{ inputs.version_name }}
runs-on: ubuntu-latest
steps:
- name: Create mocking ko
run: |
echo "${{ inputs.version }}_kernelsu.ko" > ${{ inputs.version }}_kernelsu.ko
- name: Upload LKM
uses: actions/upload-artifact@v4
if: ${{ inputs.build_lkm == true }}
with:
name: ${{ inputs.version }}-lkm
path: ./*_kernelsu.ko

261
.github/workflows/gki-kernel.yml vendored Normal file
View File

@@ -0,0 +1,261 @@
name: GKI Kernel Build
on:
workflow_call:
inputs:
version:
required: true
type: string
description: >
Output directory of gki,
for example: android12-5.10
version_name:
required: true
type: string
description: >
With SUBLEVEL of kernel,
for example: android12-5.10.66
tag:
required: true
type: string
description: >
Part of branch name of common kernel manifest,
for example: android12-5.10-2021-11
os_patch_level:
required: false
type: string
description: >
Patch level of common kernel manifest,
for example: 2021-11
default: 2022-05
patch_path:
required: false
type: string
description: >
Directory name of .github/patches/<patch_path>
for example: 5.10
use_cache:
required: false
type: boolean
default: true
embed_ksud:
required: false
type: string
default: ksud-aarch64-linux-android
description: >
Artifact name of prebuilt ksud to be embedded
for example: ksud-aarch64-linux-android
debug:
required: false
type: boolean
default: false
build_lkm:
required: false
type: boolean
default: false
secrets:
BOOT_SIGN_KEY:
required: false
CHAT_ID:
required: false
BOT_TOKEN:
required: false
MESSAGE_THREAD_ID:
required: false
jobs:
build:
name: Build ${{ inputs.version_name }}
runs-on: ubuntu-latest
env:
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
CCACHE_NOHASHDIR: "true"
CCACHE_HARDLINK: "true"
steps:
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 8192
temp-reserve-mb: 2048
remove-dotnet: 'true'
remove-android: 'true'
remove-haskell: 'true'
remove-codeql: 'true'
- uses: actions/checkout@v4
with:
path: KernelSU
fetch-depth: 0
- name: Setup need_upload
id: need_upload
run: |
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
echo "UPLOAD=true" >> $GITHUB_OUTPUT
else
echo "UPLOAD=false" >> $GITHUB_OUTPUT
fi
- name: Setup kernel source
run: |
echo "Free space:"
df -h
cd $GITHUB_WORKSPACE
sudo apt-get install repo -y
mkdir android-kernel && cd android-kernel
repo init --depth=1 --u https://android.googlesource.com/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
REMOTE_BRANCH=$(git ls-remote https://android.googlesource.com/kernel/common ${{ inputs.tag }})
DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml
if grep -q deprecated <<< $REMOTE_BRANCH; then
echo "Found deprecated branch: ${{ inputs.tag }}"
sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH
cat $DEFAULT_MANIFEST_PATH
fi
repo --version
repo --trace sync -c -j$(nproc --all) --no-tags
df -h
- name: Setup KernelSU
env:
PATCH_PATH: ${{ inputs.patch_path }}
IS_DEBUG_KERNEL: ${{ inputs.debug }}
run: |
cd $GITHUB_WORKSPACE/android-kernel
echo "[+] KernelSU setup"
GKI_ROOT=$(pwd)
echo "[+] GKI_ROOT: $GKI_ROOT"
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
echo "[+] Add KernelSU driver to Makefile"
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
echo "[+] Apply Compilation Patches"
if [ ! -e build/build.sh ]; then
GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}')
echo "GLIBC_VERSION: $GLIBC_VERSION"
if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then
echo "Patching resolve_btfids/Makefile"
cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed."
fi
fi
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
echo "[+] Enable debug features for kernel"
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
fi
repo status
echo "[+] KernelSU setup done."
- name: Symbol magic
run: |
echo "[+] Export all symbol from abi_gki_aarch64.xml"
COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common
KSU_ROOT=$GITHUB_WORKSPACE/KernelSU
ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml
SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64
# python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST
echo "[+] Add KernelSU symbols"
cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST
- name: Setup ccache
if: inputs.use_cache == true
uses: hendrikmuhs/ccache-action@v1
with:
key: gki-kernel-aarch64-${{ inputs.version_name }}
max-size: 2G
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Setup for LKM
if: ${{ inputs.build_lkm == true }}
working-directory: android-kernel
run: |
pip install ast-grep-cli
sudo apt-get install llvm-15 -y
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
# bazel build, android14-5.15, android14-6.1 use bazel
if [ ! -e build/build.sh ]; then
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
if [ -e common/modules.bzl ]; then
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
fi
else
TARGET_FILE="build/kernel/build.sh"
if [ ! -e "$TARGET_FILE" ]; then
TARGET_FILE="build/build.sh"
fi
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
fi
- name: Make working directory clean to avoid dirty
working-directory: android-kernel
run: |
if [ -e common/BUILD.bazel ]; then
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
fi
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
git config --global user.email "bot@kernelsu.org"
git config --global user.name "KernelSUBot"
cd common/ && git add -A && git commit -a -m "Add KernelSU"
repo status
- name: Build Kernel/LKM
working-directory: android-kernel
run: |
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
fi
if [ -e build/build.sh ]; then
LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang"
else
tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist
fi
- name: Prepare artifacts
id: prepareArtifacts
run: |
OUTDIR=android-kernel/out/${{ inputs.version }}/dist
if [ ! -e $OUTDIR ]; then
OUTDIR=android-kernel/dist
fi
mkdir output
if [ "${{ inputs.build_lkm}}" = "true" ]; then
llvm-strip-15 -d $OUTDIR/kernelsu.ko
mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko
else
cp $OUTDIR/Image ./output/
cp $OUTDIR/Image.lz4 ./output/
git clone https://github.com/Kernel-SU/AnyKernel3
rm -rf ./AnyKernel3/.git
cp $OUTDIR/Image ./AnyKernel3/
fi
- name: Upload Image and Image.gz
uses: actions/upload-artifact@v4
if: ${{ inputs.build_lkm == false }}
with:
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
path: ./output/*
- name: Upload AnyKernel3
if: ${{ inputs.build_lkm == false }}
uses: actions/upload-artifact@v4
with:
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
path: ./AnyKernel3/*
- name: Upload LKM
uses: actions/upload-artifact@v4
if: ${{ inputs.build_lkm == true }}
with:
name: ${{ inputs.version }}-lkm
path: ./output/*_kernelsu.ko

View File

@@ -1,23 +1,20 @@
# SukiSU
# SukiSU Ultra
**English** | [简体中文](README.md) | [日本語](README-ja.md)
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
>
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
> However, we will be a separately maintained branch of KSU in the future
>
> However, we will be a separately maintained branch of KSU in the future
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
## How to add
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
@@ -31,20 +28,17 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
1. Use the susfs-dev branch directly without any patching
## KPM support
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
## More links
Projects compiled based on Sukisu and susfs
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
@@ -53,32 +47,34 @@ Projects compiled based on Sukisu and susfs
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:**
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks.
- For Loadable Kernel Modules (LKM)
- Default hooking method for GKI kernels
- Requires `CONFIG_KPROBES=y`.
2. **Manual hooks:**
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
- Also used for Loadable Kernel Module (LKM)
- Default hook method on GKI kernels.
- Need `CONFIG_KPROBES=y`
2. **Manual hook:**
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
- Default hook method on Non-GKI kernels.
- Need `CONFIG_KSU_MANUAL_HOOK=y`
## Usage
### GKI
1. such as Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
Please follow this guide.
https://kernelsu.org/guide/installation.html
### OnePlus
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
> [!Note]
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
> - Please search for the processor codename by yourself, usually it is all English without numbers.
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
## Features
1. Kernel-based `su` and root access management.
@@ -88,21 +84,18 @@ Projects compiled based on Sukisu and susfs
5. More customization
6. Support for KPM kernel modules
## License
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
## Sponsorship list
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!

View File

@@ -1,4 +1,4 @@
# SukiSU
# SukiSU Ultra
**日本語** | [简体中文](README.md) | [English](README-en.md)
@@ -7,16 +7,15 @@
**試験中なビルドです!自己責任で使用してください!**<br>
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
>
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
>
>ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
## 追加方法
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
@@ -26,52 +25,57 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
```
## 統合された susfs の使い方
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
## KPM に対応
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
## その他のリンク
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## フックの方式
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
1. **KPROBES フック:**
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
- 読み込み可能なカーネルモジュールの場合 (LKM)
- GKI カーネルのデフォルトとなるフック方式
- `CONFIG_KPROBES=y` が必要です
- `CONFIG_KPROBES=y` が必要です
2. **手動でフック:**
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
- 非 GKI カーネル用のデフォルトフッキングメソッド
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
## 使い方
### GKI
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
このガイドに従ってください
https://kernelsu.org/ja_JP/guide/installation.html
### OnePlus
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
> [!Note]
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
## 機能
1. カーネルベースな `su` および root アクセスの管理。
@@ -81,21 +85,18 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
5. その他のカスタマイズ
6. KPM カーネルモジュールに対応
## ライセンス
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
## スポンサーシップの一覧
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。

View File

@@ -6,23 +6,19 @@
**实验性! 使用风险自负!**
>
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
> 但是我们将会在未来成为一个单独维护的KSU分支
>
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
## 如何添加
在内核源码的根目录下执行以下命令:
使用 susfs-dev 分支已集成susfs带非GKI设备的支持
使用 susfs-dev 分支(已集成 susfs带非 GKI 设备的支持)
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
使用 main 分支
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
@@ -33,46 +29,49 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
## 钩子方法
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
1. **KPROBES 钩子:**
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
- 用于可加载内核模块 (LKM)
- GKI 2.0 内核的默认钩子方法
- 需要 `CONFIG_KPROBES=y`
2. **手动钩子:**
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
- 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
- 非 GKI 内核的默认挂钩方法
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
## KPM 支持
## KPM支持
- 我们基于KernelPatch去掉了和KSU重复的功能保留了KPM支持
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
## 更多链接
基于 SukiSU 和 susfs 编译的项目
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## 使用方法
### GKI
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
3. 一般不带后缀的 .zip 压缩包是通用gz 后缀的为天玑机型专用lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
### 普适的 GKI
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
> [!Note]
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
> 3. 一般不带后缀的 .zip 压缩包是未压缩的gz 后缀的为天玑机型所使用的压缩方式
### 一加
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
> [!Note]
@@ -80,7 +79,6 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
> - 分支和配置文件请自行到一加内核开源地址进行填写
## 特点
1. 基于内核的 `su` 和 root 访问管理
@@ -88,8 +86,7 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
4. 恢复对非 GKI 2.0 内核的支持
5. 更多自定义功能
6. 对KPM内核模块的支持
6. KPM 内核模块的支持
## 许可证
@@ -97,13 +94,12 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
-`kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
## 赞助名单
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
@@ -117,4 +113,4 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
- [genuine](https://github.com/brevent/genuine/)APK v2 签名验证
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatchAPatch实现内核模块的关键部分
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatchAPatch 实现内核模块的关键部分

View File

@@ -25,11 +25,11 @@ $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
# ksu_version: major * 10000 + git version + 606 for historical reasons
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
$(info -- SukiSU version: $(KSU_VERSION))
$(info -- KernelSU version: $(KSU_VERSION))
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
else # If there is no .git file, the default version will be passed.
$(warning "KSU_GIT_VERSION not defined! It is better to make SukiSU a git submodule!")
ccflags-y += -DKSU_VERSION=12800
$(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git submodule!")
ccflags-y += -DKSU_VERSION=16
endif
ifndef KSU_EXPECTED_SIZE
@@ -42,13 +42,26 @@ endif
ifdef KSU_MANAGER_PACKAGE
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
endif
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
KERNEL_TYPE := Non-GKI
# Check for GKI 2.0 (5.10+ or 6.x+)
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
# Check for GKI 1.0 (5.4)
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
KERNEL_TYPE := GKI 1.0
endif
else
KERNEL_TYPE := GKI 2.0
endif
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
ifeq ($(CONFIG_KPM),y)
$(info -- KPM is enabled)

View File

@@ -1,5 +1,6 @@
@file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ApkSigningConfig
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.gradle.tasks.PackageAndroidArtifact
@@ -24,8 +25,18 @@ apksign {
keyPasswordProperty = "KEY_PASSWORD"
}
android {
namespace = "zako.zako.zako"
/**signingConfigs {
create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = ""
keyAlias = ""
keyPassword = ""
}
}**/
namespace = "com.sukisu.ultra"
buildTypes {
release {
@@ -33,6 +44,9 @@ android {
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
/**debug {
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
}**/
}
buildFeatures {

View File

@@ -3,8 +3,10 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application

View File

@@ -1,4 +1,4 @@
package zako.zako.zako;
package com.sukisu.zako;
import android.content.pm.PackageInfo;
import rikka.parcelablelist.ParcelableListSlice;

Binary file not shown.

View File

@@ -12,7 +12,7 @@
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
auto result = become_manager(cpkg);
env->ReleaseStringUTFChars(pkg, cpkg);
@@ -21,13 +21,13 @@ Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
extern "C"
JNIEXPORT jint JNICALL
Java_zako_zako_zako_Natives_getVersion(JNIEnv *env, jobject) {
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
return get_version();
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
int uids[1024];
int size = 0;
bool result = get_allow_list(uids, &size);
@@ -42,13 +42,13 @@ Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
return is_safe_mode();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
return is_lkm_mode();
}
@@ -111,7 +111,7 @@ static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
extern "C"
JNIEXPORT jobject JNICALL
Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
return nullptr;
}
@@ -129,7 +129,7 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
bool useDefaultProfile = !get_app_profile(key, &profile);
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
auto constructor = env->GetMethodID(cls, "<init>", "()V");
auto obj = env->NewObject(cls, constructor);
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
@@ -207,8 +207,8 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
@@ -293,16 +293,16 @@ Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject pr
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
return uid_should_umount(uid);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
return is_su_enabled();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_zako_zako_zako_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
return set_su_enabled(enabled);
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako
package com.sukisu.ultra
import android.app.Application
import coil.Coil

View File

@@ -1,4 +1,4 @@
package zako.zako.zako
package com.sukisu.ultra
import android.system.Os

View File

@@ -1,4 +1,4 @@
package zako.zako.zako
package com.sukisu.ultra
import android.os.Parcelable
import androidx.annotation.Keep
@@ -16,14 +16,14 @@ object Natives {
// 10946: add capabilities
// 10977: change groups_count and groups to avoid overflow write
// 11071: Fix the issue of failing to set a custom SELinux type.
const val MINIMAL_SUPPORTED_KERNEL = 11071
const val MINIMAL_SUPPORTED_KERNEL = 12800
// 11640: Support query working mode, LKM or GKI
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
// 12040: Support disable sucompat mode
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
const val ROOT_UID = 0

View File

@@ -0,0 +1,446 @@
package com.sukisu.ultra.flash
import android.app.Activity
import android.content.Context
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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.documentfile.provider.DocumentFile
import com.sukisu.ultra.R
import com.sukisu.ultra.utils.AssetsUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
data class FlashState(
val isFlashing: Boolean = false,
val isCompleted: Boolean = false,
val progress: Float = 0f,
val currentStep: String = "",
val logs: List<String> = emptyList(),
val error: String = ""
)
class HorizonKernelState {
private val _state = MutableStateFlow(FlashState())
val state: StateFlow<FlashState> = _state.asStateFlow()
fun updateProgress(progress: Float) {
_state.update { it.copy(progress = progress) }
}
fun updateStep(step: String) {
_state.update { it.copy(currentStep = step) }
}
fun addLog(log: String) {
_state.update {
it.copy(logs = it.logs + log)
}
}
fun setError(error: String) {
_state.update { it.copy(error = error) }
}
fun startFlashing() {
_state.update {
it.copy(
isFlashing = true,
isCompleted = false,
progress = 0f,
currentStep = "under preparation...",
logs = emptyList(),
error = ""
)
}
}
fun completeFlashing() {
_state.update { it.copy(isCompleted = true, progress = 1f) }
}
fun reset() {
_state.value = FlashState()
}
}
class HorizonKernelWorker(
private val context: Context,
private val state: HorizonKernelState,
private val slot: String? = null
) : Thread() {
var uri: Uri? = null
private lateinit var filePath: String
private lateinit var binaryPath: String
private var onFlashComplete: (() -> Unit)? = null
private var originalSlot: String? = null
fun setOnFlashCompleteListener(listener: () -> Unit) {
onFlashComplete = listener
}
override fun run() {
state.startFlashing()
state.updateStep(context.getString(R.string.horizon_preparing))
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
try {
state.updateStep(context.getString(R.string.horizon_cleaning_files))
state.updateProgress(0.1f)
cleanup()
if (!rootAvailable()) {
state.setError(context.getString(R.string.root_required))
return
}
state.updateStep(context.getString(R.string.horizon_copying_files))
state.updateProgress(0.2f)
copy()
if (!File(filePath).exists()) {
state.setError(context.getString(R.string.horizon_copy_failed))
return
}
state.updateStep(context.getString(R.string.horizon_extracting_tool))
state.updateProgress(0.4f)
getBinary()
state.updateStep(context.getString(R.string.horizon_patching_script))
state.updateProgress(0.6f)
patch()
state.updateStep(context.getString(R.string.horizon_flashing))
state.updateProgress(0.7f)
val isAbDevice = isAbDevice()
if (isAbDevice && slot != null) {
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
state.updateProgress(0.72f)
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
state.updateProgress(0.74f)
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
}
flash()
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
state.updateProgress(0.8f)
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
}
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
state.completeFlashing()
(context as? Activity)?.runOnUiThread {
onFlashComplete?.invoke()
}
} catch (e: Exception) {
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
state.updateProgress(0.8f)
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
}
}
}
// 检查设备是否为AB分区设备
private fun isAbDevice(): Boolean {
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
return false
}
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
return !slotSuffix.isNullOrEmpty()
}
private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
}
private fun copy() {
uri?.let { safeUri ->
context.contentResolver.openInputStream(safeUri)?.use { input ->
FileOutputStream(File(filePath)).use { output ->
input.copyTo(output)
}
}
}
}
private fun getBinary() {
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
if (!File(binaryPath).exists()) {
throw IOException("Failed to extract update-binary")
}
}
private fun patch() {
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
val toolName = if (version.isNotEmpty()) {
val parts = version.split('.')
if (parts.size >= 2) {
val major = parts[0].toIntOrNull() ?: 0
val minor = parts[1].toIntOrNull() ?: 0
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
} else {
"5_15+"
}
} else {
"5_15+"
}
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
}
private fun flash() {
val process = ProcessBuilder("su")
.redirectErrorStream(true)
.start()
try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
// 写入槽位信息到临时文件
slot?.let { selectedSlot ->
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
}
// 构建刷写命令
val flashCommand = buildString {
append("sh $binaryPath 3 1 \"$filePath\"")
if (slot != null) {
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
}
append(" && touch ${context.filesDir.absolutePath}/done\n")
}
writer.write(flashCommand)
writer.write("exit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.lineSequence().forEach { line ->
if (line.startsWith("ui_print")) {
val logMessage = line.removePrefix("ui_print").trim()
state.addLog(logMessage)
when {
logMessage.contains("extracting", ignoreCase = true) -> {
state.updateProgress(0.75f)
}
logMessage.contains("installing", ignoreCase = true) -> {
state.updateProgress(0.85f)
}
logMessage.contains("complete", ignoreCase = true) -> {
state.updateProgress(0.95f)
}
}
}
}
}
} finally {
process.destroy()
}
if (!File("${context.filesDir.absolutePath}/done").exists()) {
throw IOException(context.getString(R.string.flash_failed_message))
}
}
private fun runCommand(su: Boolean, cmd: String): Int {
val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true)
.start()
return try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.waitFor()
} finally {
process.destroy()
}
}
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true)
.start()
return try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.readText().trim()
}
} catch (_: Exception) {
""
} finally {
process.destroy()
}
}
private fun rootAvailable(): Boolean {
return try {
val process = Runtime.getRuntime().exec("su -c id")
val exitValue = process.waitFor()
exitValue == 0
} catch (_: Exception) {
false
}
}
}
@Composable
fun HorizonKernelFlashProgress(state: FlashState) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.horizon_flash_title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
progress = { state.progress },
)
Text(
text = state.currentStep,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 4.dp)
)
if (state.logs.isNotEmpty()) {
Text(
text = stringResource(id = R.string.horizon_logs_label),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.align(Alignment.Start)
.padding(top = 8.dp, bottom = 4.dp)
)
Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 230.dp)
.padding(vertical = 4.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp,
shape = MaterialTheme.shapes.small
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.verticalScroll(rememberScrollState())
) {
state.logs.forEach { log ->
Text(
text = log,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(vertical = 2.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
}
}
}
if (state.error.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = state.error,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
} else if (state.isCompleted) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(id = R.string.horizon_flash_complete),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.profile
package com.sukisu.ultra.profile
/**
* @author weishu

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.profile
package com.sukisu.ultra.profile
/**
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui;
package com.sukisu.ultra.ui;
import android.content.Context;
import android.content.Intent;
@@ -17,7 +17,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import zako.zako.zako.IKsuInterface;
import com.sukisu.zako.IKsuInterface;
import rikka.parcelablelist.ParcelableListSlice;
/**

View File

@@ -0,0 +1,249 @@
package com.sukisu.ultra.ui
import android.database.ContentObserver
import android.os.Build
import android.os.Bundle
import android.os.Handler
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.spec.RouteOrDirection
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import io.sukisu.ultra.UltraToolInstall
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
class MainActivity : ComponentActivity() {
private inner class ThemeChangeContentObserver(
handler: Handler,
private val onThemeChanged: () -> Unit
) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
onThemeChanged()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
if (isFirstRun) {
ThemeConfig.preventBackgroundRefresh = false
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
prefs.edit { putBoolean("is_first_run", false) }
}
// 加载保存的背景设置
loadThemeMode()
loadThemeColors()
loadDynamicColorState()
CardConfig.load(applicationContext)
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
runOnUiThread {
if (!ThemeConfig.preventBackgroundRefresh) {
ThemeConfig.backgroundImageLoaded = false
loadCustomBackground()
}
}
}
contentResolver.registerContentObserver(
android.provider.Settings.System.getUriFor("ui_night_mode"),
false,
contentObserver
)
val destroyListeners = mutableListOf<() -> Unit>()
destroyListeners.add {
contentResolver.unregisterContentObserver(contentObserver)
}
val isManager = Natives.becomeManager(ksuApp.packageName)
if (isManager) {
install()
UltraToolInstall.tryToInstall()
}
setContent {
KernelSUTheme {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
Scaffold(
bottomBar = { BottomBar(navController) },
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState
) {
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
get() = { fadeIn(animationSpec = tween(340)) }
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
get() = { fadeOut(animationSpec = tween(340)) }
}
)
}
}
}
}
}
override fun onPause() {
super.onPause()
CardConfig.save(applicationContext)
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() {
putBoolean("prevent_background_refresh", true)
}
ThemeConfig.preventBackgroundRefresh = true
}
override fun onResume() {
super.onResume()
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
loadCustomBackground()
}
}
private val destroyListeners = mutableListOf<() -> Unit>()
override fun onDestroy() {
destroyListeners.forEach { it() }
super.onDestroy()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
val kpmVersion = getKpmVersion()
val containerColor = MaterialTheme.colorScheme.surfaceVariant
val cardColor = MaterialTheme.colorScheme.surfaceVariant
NavigationBar(
modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
),
containerColor = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
).containerColor,
tonalElevation = cardElevation
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
icon = {
Icon(
imageVector = if (isCurrentDestOnBackStack) {
destination.iconSelected
} else {
destination.iconNotSelected
},
contentDescription = stringResource(destination.label),
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
},
label = {
Text(
text = stringResource(destination.label),
style = MaterialTheme.typography.labelMedium
)
}
)
}
} else {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
icon = {
Icon(
imageVector = if (isCurrentDestOnBackStack) {
destination.iconSelected
} else {
destination.iconNotSelected
},
contentDescription = stringResource(destination.label),
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
},
label = {
Text(
text = stringResource(destination.label),
style = MaterialTheme.typography.labelMedium
)
}
)
}
}
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import zako.zako.zako.BuildConfig
import zako.zako.zako.R
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.R
@Preview
@Composable

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component
package com.sukisu.ultra.ui.component
import android.graphics.text.LineBreaker
import android.os.Build

View File

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

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box

View File

@@ -0,0 +1,17 @@
package com.sukisu.ultra.ui.component
import androidx.compose.runtime.Composable
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
@Composable
fun KsuIsValid(
content: @Composable () -> Unit
) {
val isManager = Natives.becomeManager(ksuApp.packageName)
val ksuVersion = if (isManager) Natives.version else null
if (ksuVersion != null) {
content()
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component
package com.sukisu.ultra.ui.component
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import zako.zako.zako.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig
private const val TAG = "SearchBar"
@@ -63,7 +63,7 @@ fun SearchAppBar(
var onSearch by remember { mutableStateOf(false) }
// 获取卡片颜色和透明度
val cardColor = MaterialTheme.colorScheme.secondaryContainer
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
if (onSearch) {

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource

View File

@@ -0,0 +1,248 @@
package com.sukisu.ultra.ui.component
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
/**
* 槽位选择对话框组件
* 用于Kernel刷写时选择目标槽位
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SlotSelectionDialog(
show: Boolean,
onDismiss: () -> Unit,
onSlotSelected: (String) -> Unit
) {
val context = LocalContext.current
var currentSlot by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
try {
currentSlot = getCurrentSlot(context)
errorMessage = null
} catch (e: Exception) {
errorMessage = e.message
currentSlot = null
}
}
if (show) {
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.select_slot_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (errorMessage != null) {
Text(
text = "Error: $errorMessage",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
} else {
Text(
text = stringResource(
id = R.string.current_slot,
currentSlot ?: "Unknown"
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(id = R.string.select_slot_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
// Horizontal arrangement for slot options with highlighted current slot
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.SpaceBetween
) {
val slotOptions = listOf(
ListOption(
titleText = stringResource(id = R.string.slot_a),
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
icon = Icons.Filled.SdStorage
),
ListOption(
titleText = stringResource(id = R.string.slot_b),
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
icon = Icons.Filled.SdStorage
)
)
slotOptions.forEachIndexed { index, option ->
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(
color = if (option.subtitleText != null) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
} else {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
}
)
.clickable {
onSlotSelected(
when (index) {
0 -> "a"
else -> "b"
}
)
}
.padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = option.icon,
contentDescription = null,
tint = if (option.subtitleText != null) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.primary
},
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = option.titleText,
style = MaterialTheme.typography.titleMedium,
color = if (option.subtitleText != null) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.primary
}
)
option.subtitleText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = if (true) {
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}
}
}
}
}
}
},
confirmButton = {
TextButton(
onClick = {
currentSlot?.let { onSlotSelected(it) }
onDismiss()
}
) {
Text(
text = stringResource(android.R.string.ok),
color = MaterialTheme.colorScheme.primary
)
}
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
}
},
containerColor = cardColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
}
}
// Data class for list options
data class ListOption(
val titleText: String,
val subtitleText: String?,
val icon: ImageVector
)
// Utility function to get current slot
private fun getCurrentSlot(context: Context): String? {
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
if (it.startsWith("_")) it.substring(1) else it
}
}
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
return try {
val process = ProcessBuilder(if (su) "su" else "sh").start()
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.readText().trim()
}
} catch (_: Exception) {
null
}
}

View File

@@ -0,0 +1,101 @@
package com.sukisu.ultra.ui.component
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun SwitchItem(
icon: ImageVector,
title: String,
summary: String? = null,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
// 颜色动画
val iconTint by animateColorAsState(
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
animationSpec = tween(300),
label = "iconTint"
)
// 开关颜色
val switchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
checkedBorderColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
ListItem(
headlineContent = {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
supportingContent = summary?.let {
{
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis
)
}
},
leadingContent = {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = iconTint
)
},
trailingContent = {
Switch(
checked = checked,
onCheckedChange = null,
enabled = enabled,
colors = switchColors
)
},
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled) {
onCheckedChange(!checked)
}
.padding(vertical = 4.dp)
)
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component.profile
package com.sukisu.ultra.ui.component.profile
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.OutlinedTextField
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.ui.component.SwitchItem
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.SwitchItem
@Composable
fun AppProfileConfig(

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component.profile
package com.sukisu.ultra.ui.component.profile
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -42,12 +42,12 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.profile.Capabilities
import zako.zako.zako.profile.Groups
import zako.zako.zako.ui.component.rememberCustomDialog
import zako.zako.zako.ui.util.isSepolicyValid
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.profile.Capabilities
import com.sukisu.ultra.profile.Groups
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.util.isSepolicyValid
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.component.profile
package com.sukisu.ultra.ui.component.profile
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
@@ -23,11 +23,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.ui.util.listAppProfileTemplates
import zako.zako.zako.ui.util.setSepolicy
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.listAppProfileTemplates
import com.sukisu.ultra.ui.util.setSepolicy
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
/**
* @author weishu

View File

@@ -0,0 +1,593 @@
package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.width
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.AccountCircle
import androidx.compose.material.icons.filled.Android
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.SwitchItem
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
import com.sukisu.ultra.ui.component.profile.TemplateConfig
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.forceStopApp
import com.sukisu.ultra.ui.util.getSepolicy
import com.sukisu.ultra.ui.util.launchApp
import com.sukisu.ultra.ui.util.restartApp
import com.sukisu.ultra.ui.util.setSepolicy
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
import kotlinx.coroutines.launch
/**
* @author weishu
* @date 2023/5/16.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun AppProfileScreen(
navigator: DestinationsNavigator,
appInfo: SuperUserViewModel.AppInfo,
) {
val context = LocalContext.current
val snackBarHost = LocalSnackbarHost.current
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val scope = rememberCoroutineScope()
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
val packageName = appInfo.packageName
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
if (initialProfile.allowSu) {
initialProfile.rules = getSepolicy(packageName)
}
var profile by rememberSaveable {
mutableStateOf(initialProfile)
}
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
Scaffold(
topBar = {
TopBar(
title = appInfo.label,
packageName = packageName,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
onBack = dropUnlessResumed { navigator.popBackStack() },
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
AppProfileInner(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()),
packageName = appInfo.packageName,
appLabel = appInfo.label,
appIcon = {
AsyncImage(
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
contentDescription = appInfo.label,
modifier = Modifier
.padding(4.dp)
.width(48.dp)
.height(48.dp)
)
},
profile = profile,
onViewTemplate = {
getTemplateInfoById(it)?.let { info ->
navigator.navigate(TemplateEditorScreenDestination(info))
}
},
onManageTemplate = {
navigator.navigate(AppProfileTemplateScreenDestination())
},
onProfileChange = {
scope.launch {
if (it.allowSu) {
// sync with allowlist.c - forbid_system_uid
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
snackBarHost.showSnackbar(suNotAllowed)
return@launch
}
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
snackBarHost.showSnackbar(failToUpdateSepolicy)
return@launch
}
}
if (!Natives.setAppProfile(it)) {
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
} else {
profile = it
}
}
},
)
}
}
@Composable
private fun AppProfileInner(
modifier: Modifier = Modifier,
packageName: String,
appLabel: String,
appIcon: @Composable () -> Unit,
profile: Natives.Profile,
onViewTemplate: (id: String) -> Unit = {},
onManageTemplate: () -> Unit = {},
onProfileChange: (Natives.Profile) -> Unit,
) {
val isRootGranted = profile.allowSu
Column(modifier = modifier) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
AppMenuBox(packageName) {
ListItem(
headlineContent = {
Text(
text = appLabel,
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Text(
text = packageName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingContent = appIcon,
)
}
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.superuser),
checked = isRootGranted,
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
)
}
Crossfade(
targetState = isRootGranted,
label = "RootAccess"
) { current ->
Column(
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
) {
if (current) {
val initialMode = if (profile.rootUseDefault) {
Mode.Default
} else if (profile.rootTemplate != null) {
Mode.Template
} else {
Mode.Custom
}
var mode by rememberSaveable {
mutableStateOf(initialMode)
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
ProfileBox(mode, true) {
// template mode shouldn't change profile here!
if (it == Mode.Default || it == Mode.Custom) {
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
}
mode = it
}
}
AnimatedVisibility(
visible = mode != Mode.Default,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
when (currentMode) {
Mode.Template -> {
TemplateConfig(
profile = profile,
onViewTemplate = onViewTemplate,
onManageTemplate = onManageTemplate,
onProfileChange = onProfileChange
)
}
Mode.Custom -> {
RootProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = onProfileChange
)
}
else -> {}
}
}
}
}
}
} else {
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
ProfileBox(mode, false) {
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
}
}
AnimatedVisibility(
visible = mode == Mode.Custom,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
AppProfileConfig(
fixedName = true,
profile = profile,
enabled = mode == Mode.Custom,
onProfileChange = onProfileChange
)
}
}
}
}
}
}
}
}
private enum class Mode(@StringRes private val res: Int) {
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
val text: String
@Composable get() = stringResource(res)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
title: String,
packageName: String,
onBack: () -> Unit,
colors: TopAppBarColors,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TopAppBar(
title = {
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = packageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.alpha(0.8f)
)
}
},
colors = colors,
navigationIcon = {
IconButton(
onClick = onBack,
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
windowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
scrollBehavior = scrollBehavior,
modifier = Modifier.shadow(
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
4.dp else 0.dp,
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
)
)
}
@Composable
private fun ProfileBox(
mode: Mode,
hasTemplate: Boolean,
onModeChange: (Mode) -> Unit,
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.profile),
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Text(
text = mode.text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingContent = {
Icon(
imageVector = Icons.Filled.AccountCircle,
contentDescription = null,
)
},
)
HorizontalDivider(
thickness = Dp.Hairline,
color = MaterialTheme.colorScheme.outlineVariant
)
ListItem(
headlineContent = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) {
FilterChip(
selected = mode == Mode.Default,
onClick = { onModeChange(Mode.Default) },
label = {
Text(
text = stringResource(R.string.profile_default),
style = MaterialTheme.typography.bodyMedium
)
},
shape = MaterialTheme.shapes.small
)
if (hasTemplate) {
FilterChip(
selected = mode == Mode.Template,
onClick = { onModeChange(Mode.Template) },
label = {
Text(
text = stringResource(R.string.profile_template),
style = MaterialTheme.typography.bodyMedium
)
},
shape = MaterialTheme.shapes.small
)
}
FilterChip(
selected = mode == Mode.Custom,
onClick = { onModeChange(Mode.Custom) },
label = {
Text(
text = stringResource(R.string.profile_custom),
style = MaterialTheme.typography.bodyMedium
)
},
shape = MaterialTheme.shapes.small
)
}
}
)
}
}
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
var expanded by remember { mutableStateOf(false) }
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
val density = LocalDensity.current
BoxWithConstraints(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
touchPoint = it
expanded = true
}
)
}
) {
content()
val (offsetX, offsetY) = with(density) {
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
}
DropdownMenu(
expanded = expanded,
offset = DpOffset(offsetX, -offsetY),
onDismissRequest = {
expanded = false
},
) {
AppMenuOption(
text = stringResource(id = R.string.launch_app),
onClick = {
expanded = false
launchApp(packageName)
}
)
AppMenuOption(
text = stringResource(id = R.string.force_stop_app),
onClick = {
expanded = false
forceStopApp(packageName)
}
)
AppMenuOption(
text = stringResource(id = R.string.restart_app),
onClick = {
expanded = false
restartApp(packageName)
}
)
}
}
}
@Composable
private fun AppMenuOption(text: String, onClick: () -> Unit) {
DropdownMenuItem(
text = {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium
)
},
onClick = onClick
)
}
@Preview
@Composable
private fun AppProfilePreview() {
var profile by remember { mutableStateOf(Natives.Profile("")) }
Surface {
AppProfileInner(
packageName = "icu.nullptr.test",
appLabel = "Test",
appIcon = {
Icon(
imageVector = Icons.Filled.Android,
contentDescription = null,
)
},
profile = profile,
onProfileChange = {
profile = it
},
)
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
@@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
import zako.zako.zako.R
import com.sukisu.ultra.R
enum class BottomBarDestination(
val direction: DirectionDestinationSpec,

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import android.os.Environment
import androidx.compose.foundation.layout.Column
@@ -37,10 +37,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.R
import zako.zako.zako.ui.component.KeyEventBlocker
import zako.zako.zako.ui.util.LocalSnackbarHost
import zako.zako.zako.ui.util.runModuleAction
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.KeyEventBlocker
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.runModuleAction
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import android.net.Uri
import android.os.Environment
@@ -30,9 +30,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import zako.zako.zako.ui.component.KeyEventBlocker
import zako.zako.zako.ui.util.*
import zako.zako.zako.R
import com.sukisu.ultra.ui.component.KeyEventBlocker
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.R
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@@ -45,10 +45,6 @@ enum class FlashingStatus {
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
fun getFlashingStatus(): FlashingStatus {
return currentFlashingStatus.value
}
fun setFlashingStatus(status: FlashingStatus) {
currentFlashingStatus.value = status
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,788 @@
package com.sukisu.ultra.ui.screen
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
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.AutoFixHigh
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.RadioButtonDefaults
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.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
import com.sukisu.ultra.flash.HorizonKernelState
import com.sukisu.ultra.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.util.LkmSelection
import com.sukisu.ultra.ui.util.getCurrentKmi
import com.sukisu.ultra.ui.util.getSupportedKmis
import com.sukisu.ultra.ui.util.isAbDevice
import com.sukisu.ultra.ui.util.isInitBoot
import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.getKernelVersion
/**
* @author weishu
* @date 2024/3/12.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun InstallScreen(navigator: DestinationsNavigator) {
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val horizonKernelState = remember { HorizonKernelState() }
val flashState by horizonKernelState.state.collectAsState()
val summary = stringResource(R.string.horizon_kernel_summary)
val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice()
val onFlashComplete = {
showRebootDialog = true
}
if (showRebootDialog) {
RebootDialog(
show = true,
onDismiss = { showRebootDialog = false },
onConfirm = {
showRebootDialog = false
try {
val process = Runtime.getRuntime().exec("su")
process.outputStream.bufferedWriter().use { writer ->
writer.write("svc power reboot\n")
writer.write("exit\n")
}
} catch (_: Exception) {
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
}
}
)
}
val onInstall = {
installMethod?.let { method ->
when (method) {
is InstallMethod.HorizonKernel -> {
method.uri?.let { uri ->
val worker = HorizonKernelWorker(
context = context,
state = horizonKernelState,
slot = method.slot
)
worker.uri = uri
worker.setOnFlashCompleteListener(onFlashComplete)
worker.start()
}
}
else -> {
val flashIt = FlashIt.FlashBoot(
boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot
)
navigator.navigate(FlashScreenDestination(flashIt))
}
}
}
Unit
}
// 槽位选择
SlotSelectionDialog(
show = showSlotSelectionDialog && isAbDevice,
onDismiss = { showSlotSelectionDialog = false },
onSlotSelected = { slot ->
showSlotSelectionDialog = false
val horizonMethod = InstallMethod.HorizonKernel(
uri = tempKernelUri,
slot = slot,
summary = summary
)
installMethod = horizonMethod
}
)
val currentKmi by produceState(initialValue = "") {
value = getCurrentKmi()
}
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
kmi?.let {
lkmSelection = LkmSelection.KmiString(it)
onInstall()
}
}
val onClickNext = {
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
selectKmiDialog.show()
} else {
onInstall()
}
}
val selectLkmLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
lkmSelection = LkmSelection.LkmUri(uri)
}
}
}
val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
})
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
TopBar(
onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
.padding(top = 12.dp)
) {
SelectInstallMethod(
isGKI = isGKI,
isAbDevice = isAbDevice,
onSelected = { method ->
if (method is InstallMethod.HorizonKernel && method.uri != null) {
if (isAbDevice) {
tempKernelUri = method.uri
showSlotSelectionDialog = true
} else {
installMethod = method
}
} else {
installMethod = method
}
horizonKernelState.reset()
}
)
AnimatedVisibility(
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
HorizonKernelFlashProgress(flashState)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
(lkmSelection as? LkmSelection.LkmUri)?.let {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = stringResource(
id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)"
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
}
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
if (method.slot != null) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = stringResource(
id = R.string.selected_slot,
if (method.slot == "a") stringResource(id = R.string.slot_a)
else stringResource(id = R.string.slot_b)
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp)
)
}
}
}
Button(
modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null && !flashState.isFlashing,
onClick = onClickNext,
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
) {
Text(
stringResource(id = R.string.install_next),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
@Composable
private fun RebootDialog(
show: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
if (show) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(id = R.string.yes))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.no))
}
}
)
}
}
sealed class InstallMethod {
data class SelectFile(
val uri: Uri? = null,
@StringRes override val label: Int = R.string.select_file,
override val summary: String?
) : InstallMethod()
data object DirectInstall : InstallMethod() {
override val label: Int
get() = R.string.direct_install
}
data object DirectInstallToInactiveSlot : InstallMethod() {
override val label: Int
get() = R.string.install_inactive_slot
}
data class HorizonKernel(
val uri: Uri? = null,
val slot: String? = null,
@StringRes override val label: Int = R.string.horizon_kernel,
override val summary: String? = null
) : InstallMethod()
abstract val label: Int
open val summary: String? = null
}
@Composable
private fun SelectInstallMethod(
isGKI: Boolean = false,
isAbDevice: Boolean = false,
onSelected: (InstallMethod) -> Unit = {}
) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
val selectFileTip = stringResource(
id = R.string.select_file_tip,
if (isInitBoot()) "init_boot" else "boot"
)
val radioOptions = mutableListOf<InstallMethod>(
InstallMethod.SelectFile(summary = selectFileTip)
)
if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall)
if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
}
radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary))
}
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
val option = when (currentSelectingMethod) {
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
uri,
summary = selectFileTip
)
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
uri,
summary = horizonKernelSummary
)
else -> null
}
option?.let {
selectedOption = it
onSelected(it)
}
}
}
}
val confirmDialog = rememberConfirmDialog(
onConfirm = {
selectedOption = InstallMethod.DirectInstallToInactiveSlot
onSelected(InstallMethod.DirectInstallToInactiveSlot)
},
onDismiss = null
)
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
val onClick = { option: InstallMethod ->
currentSelectingMethod = option
when (option) {
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/*"
putExtra(
Intent.EXTRA_MIME_TYPES,
arrayOf("application/octet-stream", "application/zip")
)
})
}
is InstallMethod.DirectInstall -> {
selectedOption = option
onSelected(option)
}
is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent)
}
}
}
var LKMExpanded by remember { mutableStateOf(false) }
var GKIExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
// LKM 安装/修补
if (isGKI) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
ListItem(
leadingContent = {
Icon(
Icons.Filled.AutoFixHigh,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
headlineContent = {
Text(
stringResource(R.string.Lkm_install_methods),
style = MaterialTheme.typography.titleMedium
)
},
modifier = Modifier.clickable {
LKMExpanded = !LKMExpanded
}
)
AnimatedVisibility(
visible = LKMExpanded,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp
)
) {
radioOptions.take(3).forEach { option ->
val interactionSource = remember { MutableInteractionSource() }
Surface(
color = if (option.javaClass == selectedOption?.javaClass)
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
else
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.toggleable(
value = option.javaClass == selectedOption?.javaClass,
onValueChange = { onClick(option) },
role = Role.RadioButton,
indication = LocalIndication.current,
interactionSource = interactionSource
)
.padding(vertical = 8.dp, horizontal = 12.dp)
) {
RadioButton(
selected = option.javaClass == selectedOption?.javaClass,
onClick = null,
interactionSource = interactionSource,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colorScheme.primary,
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Text(
text = stringResource(id = option.label),
style = MaterialTheme.typography.bodyLarge
)
option.summary?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
}
// anykernel3 刷写
if (rootAvailable) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
ListItem(
leadingContent = {
Icon(
Icons.Filled.FileUpload,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
headlineContent = {
Text(
stringResource(R.string.GKI_install_methods),
style = MaterialTheme.typography.titleMedium
)
},
modifier = Modifier.clickable {
GKIExpanded = !GKIExpanded
}
)
AnimatedVisibility(
visible = GKIExpanded,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp
)
) {
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
val interactionSource = remember { MutableInteractionSource() }
Surface(
color = if (option.javaClass == selectedOption?.javaClass)
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
else
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.toggleable(
value = option.javaClass == selectedOption?.javaClass,
onValueChange = { onClick(option) },
role = Role.RadioButton,
indication = LocalIndication.current,
interactionSource = interactionSource
)
.padding(vertical = 8.dp, horizontal = 12.dp)
) {
RadioButton(
selected = option.javaClass == selectedOption?.javaClass,
onClick = null,
interactionSource = interactionSource,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colorScheme.primary,
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
Column(
modifier = Modifier
.padding(start = 10.dp)
.weight(1f)
) {
Text(
text = stringResource(id = option.label),
style = MaterialTheme.typography.bodyLarge
)
option.summary?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val supportedKmi by produceState(initialValue = emptyList<String>()) {
value = getSupportedKmis()
}
val options = supportedKmi.map { value ->
ListOption(
titleText = value
)
}
var selection by remember { mutableStateOf<String?>(null) }
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor
)
) {
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
onSelected(selection)
}, onCloseRequest = {
dismiss()
}), header = Header.Default(
title = stringResource(R.string.select_kmi),
), selection = ListSelection.Single(
showRadioButtons = true,
options = options,
) { _, option ->
selection = option.titleText
})
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
onBack: () -> Unit = {},
onLkmUpload: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = cardAlpha
TopAppBar(
title = {
Text(
stringResource(R.string.install),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
windowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
scrollBehavior = scrollBehavior
)
}
@Preview
@Composable
fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator)
}

View File

@@ -0,0 +1,783 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
import com.sukisu.ultra.ui.util.*
import java.io.File
import androidx.core.content.edit
import com.sukisu.ultra.R
import java.io.BufferedReader
import java.io.FileInputStream
import java.io.InputStreamReader
import java.net.*
import android.app.Activity
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
/**
* KPM 管理界面
* 以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能
* 开发者ShirkNeko, Liaokong
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun KpmScreen(
navigator: DestinationsNavigator,
viewModel: KpmViewModel = viewModel()
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog()
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
val moduleFileName = module.id
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
val cancel = stringResource(R.string.cancel)
val uninstall = stringResource(R.string.uninstall)
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
val installModeDialog = rememberCustomDialog { dismiss ->
var moduleName by remember { mutableStateOf<String?>(null) }
LaunchedEffect(tempFileForInstall) {
tempFileForInstall?.let { tempFile ->
try {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
val process = Runtime.getRuntime().exec(command)
val inputStream = process.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("name=")) {
moduleName = line.substringAfter("name=").trim()
break
}
}
process.waitFor()
} catch (e: Exception) {
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
}
}
}
AlertDialog(
onDismissRequest = {
dismiss()
tempFileForInstall?.delete()
tempFileForInstall = null
},
title = {
Text(
text = kpmInstallMode,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Column {
moduleName?.let {
Text(
text = stringResource(R.string.kpm_install_mode_description, it),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(16.dp))
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
scope.launch {
dismiss()
tempFileForInstall?.let { tempFile ->
handleModuleInstall(
tempFile = tempFile,
isEmbed = false,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmInstallSuccess = kpmInstallSuccess,
kpmInstallFailed = kpmInstallFailed
)
}
tempFileForInstall = null
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Filled.Download,
contentDescription = null,
modifier = Modifier.size(18.dp).padding(end = 4.dp)
)
Text(kpmInstallModeLoad)
}
Button(
onClick = {
scope.launch {
dismiss()
tempFileForInstall?.let { tempFile ->
handleModuleInstall(
tempFile = tempFile,
isEmbed = true,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmInstallSuccess = kpmInstallSuccess,
kpmInstallFailed = kpmInstallFailed
)
}
tempFileForInstall = null
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Icon(
imageVector = Icons.Filled.Inventory,
contentDescription = null,
modifier = Modifier.size(18.dp).padding(end = 4.dp)
)
Text(kpmInstallModeEmbed)
}
}
}
},
confirmButton = {
},
dismissButton = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
TextButton(
onClick = {
dismiss()
tempFileForInstall?.delete()
tempFileForInstall = null
}
) {
Text(cancel)
}
}
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge
)
}
val selectPatchLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
scope.launch {
val fileName = uri.lastPathSegment ?: "unknown.kpm"
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
val tempFile = File(context.cacheDir, encodedFileName)
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
val mimeType = context.contentResolver.getType(uri)
val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream")
if (!isCorrectMimeType) {
var shouldShowSnackbar = true
try {
val matchCount = checkStringsCommand(tempFile)
val isElf = isElfFile(tempFile)
if (matchCount >= 1 || isElf) {
shouldShowSnackbar = false
}
} catch (e: Exception) {
Log.e("KsuCli", "Failed to execute checks: ${e.message}", e)
}
if (shouldShowSnackbar) {
snackBarHost.showSnackbar(
message = invalidFileTypeMessage,
duration = SnackbarDuration.Short
)
}
tempFile.delete()
return@launch
}
tempFileForInstall = tempFile
installModeDialog.show()
}
}
LaunchedEffect(Unit) {
while(true) {
viewModel.fetchModuleList()
delay(5000)
}
}
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.kpm_title)) },
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
scrollBehavior = scrollBehavior,
dropdownContent = {
IconButton(
onClick = { viewModel.fetchModuleList() }
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.refresh),
)
}
}
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
selectPatchLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
}
)
},
icon = {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = stringResource(R.string.kpm_install),
)
},
text = {
Text(
text = stringResource(R.string.kpm_install),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
},
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
)
},
snackbarHost = { SnackbarHost(snackBarHost) }
) { padding ->
Column(modifier = Modifier.padding(padding)) {
if (!isNoticeClosed) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(MaterialTheme.shapes.medium)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = null,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Text(
text = stringResource(R.string.kernel_module_notice),
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
IconButton(
onClick = {
isNoticeClosed = true
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
},
modifier = Modifier.size(24.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.close_notice)
)
}
}
}
}
if (viewModel.moduleList.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Code,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
stringResource(R.string.kpm_empty),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(viewModel.moduleList) { module ->
KpmModuleItem(
module = module,
onUninstall = {
scope.launch {
val confirmContent = moduleConfirmContentMap[module.id] ?: ""
handleModuleUninstall(
module = module,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmUninstallSuccess = kpmUninstallSuccess,
kpmUninstallFailed = kpmUninstallFailed,
failedToCheckModuleFile = failedToCheckModuleFile,
uninstall = uninstall,
cancel = cancel,
confirmDialog = confirmDialog,
confirmTitle = confirmTitle,
confirmContent = confirmContent
)
}
},
onControl = {
viewModel.loadModuleDetail(module.id)
}
)
}
}
}
}
}
}
private suspend fun handleModuleInstall(
tempFile: File,
isEmbed: Boolean,
viewModel: KpmViewModel,
snackBarHost: SnackbarHostState,
kpmInstallSuccess: String,
kpmInstallFailed: String
) {
var moduleId: String? = null
try {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
val process = Runtime.getRuntime().exec(command)
val inputStream = process.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line!!.startsWith("name=")) {
moduleId = line.substringAfter("name=").trim()
break
}
}
process.waitFor()
} catch (e: Exception) {
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
}
if (moduleId == null || moduleId.isEmpty()) {
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
tempFile.delete()
return
}
val targetPath = "/data/adb/kpm/$moduleId.kpm"
try {
if (isEmbed) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
}
val loadResult = loadKpmModule(tempFile.absolutePath)
if (loadResult.startsWith("Error")) {
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
} else {
viewModel.fetchModuleList()
snackBarHost.showSnackbar(
message = kpmInstallSuccess,
duration = SnackbarDuration.Short
)
}
} catch (e: Exception) {
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
}
tempFile.delete()
}
private suspend fun handleModuleUninstall(
module: KpmViewModel.ModuleInfo,
viewModel: KpmViewModel,
snackBarHost: SnackbarHostState,
kpmUninstallSuccess: String,
kpmUninstallFailed: String,
failedToCheckModuleFile: String,
uninstall: String,
cancel: String,
confirmTitle : String,
confirmContent : String,
confirmDialog: ConfirmDialogHandle
) {
val moduleFileName = "${module.id}.kpm"
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
val fileExists = try {
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
result
} catch (e: Exception) {
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
snackBarHost.showSnackbar(
message = failedToCheckModuleFile,
duration = SnackbarDuration.Short
)
false
}
val confirmResult = confirmDialog.awaitConfirm(
title = confirmTitle,
content = confirmContent,
confirm = uninstall,
dismiss = cancel
)
if (confirmResult == ConfirmResult.Confirmed) {
try {
val unloadResult = unloadKpmModule(module.id)
if (unloadResult.startsWith("Error")) {
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Short
)
return
}
if (fileExists) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
}
viewModel.fetchModuleList()
snackBarHost.showSnackbar(
message = kpmUninstallSuccess,
duration = SnackbarDuration.Short
)
} catch (e: Exception) {
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Short
)
}
}
}
@Composable
private fun KpmModuleItem(
module: KpmViewModel.ModuleInfo,
onUninstall: () -> Unit,
onControl: () -> Unit
) {
val viewModel: KpmViewModel = viewModel()
val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
val successMessage = stringResource(R.string.kpm_control_success)
val failureMessage = stringResource(R.string.kpm_control_failed)
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
AlertDialog(
onDismissRequest = { viewModel.hideInputDialog() },
title = {
Text(
text = stringResource(R.string.kpm_control),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
OutlinedTextField(
value = viewModel.inputArgs,
onValueChange = { viewModel.updateInputArgs(it) },
label = {
Text(
text = stringResource(R.string.kpm_args),
color = MaterialTheme.colorScheme.primary
)
},
placeholder = {
Text(
text = module.args,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
},
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
)
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
val result = viewModel.executeControl()
val message = when (result) {
0 -> successMessage
else -> failureMessage
}
snackBarHost.showSnackbar(message)
onControl()
}
}
) {
Text(
text = stringResource(R.string.confirm),
color = MaterialTheme.colorScheme.primary
)
}
},
dismissButton = {
TextButton(onClick = { viewModel.hideInputDialog() }) {
Text(
text = stringResource(R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
}
},
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge
)
}
Card(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Column(
modifier = Modifier.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = module.name,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = module.description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { viewModel.showInputDialog(module.id) },
enabled = module.hasAction,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.kpm_control))
}
Button(
onClick = onUninstall,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.kpm_uninstall))
}
}
}
}
}
private fun checkStringsCommand(tempFile: File): Int {
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
val process = Runtime.getRuntime().exec(command)
val inputStream = process.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
var matchCount = 0
val keywords = listOf("name=", "version=", "license=", "author=")
var nameExists = false
while (reader.readLine().also { line = it } != null) {
if (!nameExists && line!!.startsWith("name=")) {
nameExists = true
matchCount++
} else if (nameExists) {
for (keyword in keywords) {
if (line!!.startsWith(keyword)) {
matchCount++
break
}
}
}
}
process.waitFor()
return if (nameExists) matchCount else 0
}
private fun isElfFile(tempFile: File): Boolean {
val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
val fileBytes = ByteArray(4)
FileInputStream(tempFile).use { input ->
input.read(fileBytes)
}
return fileBytes.contentEquals(elfMagic)
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import android.app.Activity.*
import android.content.Context
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
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.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.toggleable
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
@@ -88,32 +46,31 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.Natives
import zako.zako.zako.ui.component.ConfirmResult
import zako.zako.zako.ui.component.SearchAppBar
import zako.zako.zako.ui.component.rememberConfirmDialog
import zako.zako.zako.ui.component.rememberLoadingDialog
import zako.zako.zako.ui.util.DownloadListener
import zako.zako.zako.ui.util.*
import zako.zako.zako.ui.util.download
import zako.zako.zako.ui.util.hasMagisk
import zako.zako.zako.ui.util.reboot
import zako.zako.zako.ui.util.restoreModule
import zako.zako.zako.ui.util.toggleModule
import zako.zako.zako.ui.util.uninstallModule
import zako.zako.zako.ui.webui.WebUIActivity
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.component.SearchAppBar
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberLoadingDialog
import com.sukisu.ultra.ui.util.DownloadListener
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.download
import com.sukisu.ultra.ui.util.hasMagisk
import com.sukisu.ultra.ui.util.reboot
import com.sukisu.ultra.ui.util.restoreModule
import com.sukisu.ultra.ui.util.toggleModule
import com.sukisu.ultra.ui.util.uninstallModule
import com.sukisu.ultra.ui.webui.WebUIActivity
import okhttp3.OkHttpClient
import zako.zako.zako.ui.util.ModuleModify
import zako.zako.zako.ui.theme.getCardColors
import zako.zako.zako.ui.theme.getCardElevation
import zako.zako.zako.ui.viewmodel.ModuleViewModel
import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.zip.ZipInputStream
import androidx.core.content.edit
import androidx.core.net.toUri
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.R
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
@OptIn(ExperimentalMaterial3Api::class)
@@ -239,7 +196,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
@@ -275,7 +232,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings)
contentDescription = stringResource(id = R.string.settings),
)
DropdownMenu(
@@ -284,7 +241,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_action_first)) },
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
trailingIcon = {
Checkbox(
checked = viewModel.sortActionFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = {
viewModel.sortActionFirst = !viewModel.sortActionFirst
prefs.edit {
@@ -300,23 +266,33 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
)
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
trailingIcon = {
Checkbox(
checked = viewModel.sortEnabledFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = {
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
prefs.edit {
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
}
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
}
scope.launch {
viewModel.fetchModuleList()
}
}
)
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem(
text = { Text(stringResource(R.string.backup_modules)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Download,
contentDescription = stringResource(R.string.backup)
contentDescription = stringResource(R.string.backup),
)
},
onClick = {
@@ -329,7 +305,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.restore)
contentDescription = stringResource(R.string.restore),
)
},
onClick = {
@@ -346,11 +322,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
floatingActionButton = {
if (!hideInstallButton) {
val moduleInstall = stringResource(id = R.string.module_install)
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
ExtendedFloatingActionButton(
onClick = {
selectZipLauncher.launch(
@@ -363,16 +334,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
icon = {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = moduleInstall
contentDescription = moduleInstall,
)
},
text = {
Text(
text = moduleInstall
text = moduleInstall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
},
containerColor = cardColor.copy(alpha = 1f),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
)
}
},
@@ -389,10 +361,25 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.module_magisk_conflict),
textAlign = TextAlign.Center,
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier
.size(64.dp)
.padding(bottom = 16.dp)
)
Text(
stringResource(R.string.module_magisk_conflict),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
else -> {
@@ -591,10 +578,25 @@ private fun ModuleList(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.module_empty),
textAlign = TextAlign.Center
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Outlined.Extension,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
text = stringResource(R.string.module_empty),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@@ -677,8 +679,16 @@ fun ModuleItem(
onClick: (ModuleViewModel.ModuleInfo) -> Unit
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
val interactionSource = remember { MutableInteractionSource() }
@@ -706,6 +716,7 @@ fun ModuleItem(
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val moduleVersion = stringResource(id = R.string.module_version)
val moduleAuthor = stringResource(id = R.string.module_author)
@@ -720,6 +731,7 @@ fun ModuleItem(
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurface
)
Text(
@@ -727,7 +739,8 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.bodySmall.fontSize,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration
textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
@@ -735,7 +748,8 @@ fun ModuleItem(
fontSize = MaterialTheme.typography.bodySmall.fontSize,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration
textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
@@ -749,7 +763,15 @@ fun ModuleItem(
enabled = !module.update,
checked = module.enabled,
onCheckedChange = onCheckChanged,
interactionSource = if (!module.hasWebUi) interactionSource else null
interactionSource = if (!module.hasWebUi) interactionSource else null,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
}
@@ -764,83 +786,70 @@ fun ModuleItem(
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
overflow = TextOverflow.Ellipsis,
maxLines = 4,
textDecoration = textDecoration
textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(thickness = Dp.Hairline)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (module.hasActionScript) {
FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove && module.enabled,
onClick = {
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
viewModel.markNeedRefresh()
},
contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) {
ButtonDefaults.filledTonalButtonColors(
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
colors = ButtonDefaults.filledTonalButtonColors()
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.PlayArrow,
contentDescription = null
)
if (!module.hasWebUi && updateUrl.isEmpty()) {
Text(
modifier = Modifier.padding(start = 7.dp),
text = stringResource(R.string.action),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize
)
}
//if (!module.hasWebUi && updateUrl.isEmpty()) {
//Text(
// modifier = Modifier.padding(start = 7.dp),
// text = stringResource(R.string.action),
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
// fontSize = MaterialTheme.typography.labelMedium.fontSize
//)
//}
}
Spacer(modifier = Modifier.weight(0.1f, true))
}
if (module.hasWebUi) {
FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove && module.enabled,
onClick = { onClick(module) },
interactionSource = interactionSource,
contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) {
ButtonDefaults.filledTonalButtonColors(
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
colors = ButtonDefaults.filledTonalButtonColors()
) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
contentDescription = null
)
if (!module.hasActionScript && updateUrl.isEmpty()) {
Text(
modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.open)
)
}
//if (!module.hasActionScript && updateUrl.isEmpty()) {
//Text(
// modifier = Modifier.padding(start = 7.dp),
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
// text = stringResource(R.string.open)
//)
//}
}
}
@@ -848,7 +857,7 @@ fun ModuleItem(
if (updateUrl.isNotEmpty()) {
Button(
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
enabled = !module.remove,
onClick = { onUpdate(module) },
shape = ButtonDefaults.textShape,
@@ -859,30 +868,23 @@ fun ModuleItem(
imageVector = Icons.Outlined.Download,
contentDescription = null
)
if (!module.hasActionScript || !module.hasWebUi) {
Text(
modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(R.string.module_update)
)
}
//if (!module.hasActionScript || !module.hasWebUi) {
//Text(
// modifier = Modifier.padding(start = 7.dp),
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
// text = stringResource(R.string.module_update)
//)
//}
}
Spacer(modifier = Modifier.weight(0.1f, true))
}
FilledTonalButton(
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
onClick = { onUninstallClicked(module) },
contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = if (!ThemeConfig.useDynamicColor) {
ButtonDefaults.filledTonalButtonColors(
containerColor = ThemeConfig.currentTheme.ButtonContrast
)
} else {
ButtonDefaults.filledTonalButtonColors()
}
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
) {
if (!module.remove) {
Icon(
@@ -894,18 +896,18 @@ fun ModuleItem(
Icon(
modifier = Modifier.size(20.dp).rotate(180f),
imageVector = Icons.Outlined.Refresh,
contentDescription = null,
)
}
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
Text(
modifier = Modifier.padding(start = 7.dp),
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
fontSize = MaterialTheme.typography.labelMedium.fontSize,
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
contentDescription = null
)
}
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
//Text(
// modifier = Modifier.padding(start = 7.dp),
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
//)
//}
}
}
}
@@ -932,4 +934,3 @@ fun ModuleItemPreview() {
)
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
}

View File

@@ -0,0 +1,925 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
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.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
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.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.material.icons.automirrored.filled.NavigateNext
import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.ColorLens
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Opacity
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Wallpaper
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.component.ImageEditorDialog
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SwitchItem
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeColors
import com.sukisu.ultra.ui.theme.ThemeConfig
import com.sukisu.ultra.ui.theme.saveAndApplyCustomBackground
import com.sukisu.ultra.ui.theme.saveCustomBackground
import com.sukisu.ultra.ui.theme.saveDynamicColorState
import com.sukisu.ultra.ui.theme.saveThemeColors
import com.sukisu.ultra.ui.theme.saveThemeMode
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.getSuSFSFeatures
import com.sukisu.ultra.ui.util.susfsSUS_SU_0
import com.sukisu.ultra.ui.util.susfsSUS_SU_2
import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
fun saveCardConfig(context: Context) {
CardConfig.save(context)
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val systemIsDark = isSystemInDarkTheme()
// 主题模式选择
var themeMode by remember {
mutableIntStateOf(
when(ThemeConfig.forceDarkMode) {
true -> 2 // 深色
false -> 1 // 浅色
null -> 0 // 跟随系统
}
)
}
// 动态颜色开关状态
var useDynamicColor by remember {
mutableStateOf(ThemeConfig.useDynamicColor)
}
var showThemeModeDialog by remember { mutableStateOf(false) }
// 主题模式选项
val themeOptions = listOf(
stringResource(R.string.theme_follow_system),
stringResource(R.string.theme_light),
stringResource(R.string.theme_dark)
)
// 简洁模式开关状态
var isSimpleMode by remember {
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
}
// 更新简洁模式开关状态
val onSimpleModeChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_simple_mode", newValue) }
isSimpleMode = newValue
}
// 隐藏内核版本号开关状态
var isHideVersion by remember {
mutableStateOf(prefs.getBoolean("is_hide_version", false))
}
// 隐藏内核版本号开关状态
val onHideVersionChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_version", newValue) }
isHideVersion = newValue
}
// 隐藏模块数量等信息开关状态
var isHideOtherInfo by remember {
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
}
// 隐藏模块数量等信息开关状态
val onHideOtherInfoChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_other_info", newValue) }
isHideOtherInfo = newValue
}
// 隐藏SuSFS状态开关状态
var isHideSusfsStatus by remember {
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
}
// 隐藏SuSFS状态开关状态
val onHideSusfsStatusChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
isHideSusfsStatus = newValue
}
// 隐藏链接状态开关状态
var isHideLinkCard by remember {
mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
}
val onHideLinkCardChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_link_card", newValue) }
isHideLinkCard = newValue
}
// SELinux状态
var selinuxEnabled by remember {
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
}
// 卡片配置状态
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
var isCustomBackgroundEnabled by rememberSaveable {
mutableStateOf(ThemeConfig.customBackgroundUri != null)
}
// 图片编辑状态
var showImageEditor by remember { mutableStateOf(false) }
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
// 展开/折叠状态
var isCustomizeExpanded by remember { mutableStateOf(false) }
var isAppearanceExpanded by remember { mutableStateOf(true) }
var isAdvancedExpanded by remember { mutableStateOf(false) }
// 初始化卡片配置
LaunchedEffect(Unit) {
// 加载设置
CardConfig.load(context)
cardAlpha = CardConfig.cardAlpha
isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null
// 设置主题模式
themeMode = when (ThemeConfig.forceDarkMode) {
true -> 2
false -> 1
null -> 0
}
// 确保卡片样式跟随主题模式
when (themeMode) {
2 -> { // 深色
CardConfig.isUserDarkModeEnabled = true
CardConfig.isUserLightModeEnabled = false
}
1 -> { // 浅色
CardConfig.isUserDarkModeEnabled = false
CardConfig.isUserLightModeEnabled = true
}
0 -> { // 跟随系统
CardConfig.isUserDarkModeEnabled = false
CardConfig.isUserLightModeEnabled = false
}
}
// 如果启用了系统跟随且系统是深色模式,应用深色模式默认值
if (themeMode == 0 && systemIsDark) {
CardConfig.setDarkModeDefaults()
}
// 保存设置
CardConfig.save(context)
}
// 主题色选项
val themeColorOptions = listOf(
stringResource(R.string.color_default) to ThemeColors.Default,
stringResource(R.string.color_green) to ThemeColors.Green,
stringResource(R.string.color_purple) to ThemeColors.Purple,
stringResource(R.string.color_orange) to ThemeColors.Orange,
stringResource(R.string.color_pink) to ThemeColors.Pink,
stringResource(R.string.color_gray) to ThemeColors.Gray,
stringResource(R.string.color_yellow) to ThemeColors.Yellow
)
var showThemeColorDialog by remember { mutableStateOf(false) }
// 图片选择器
val pickImageLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
selectedImageUri = it
showImageEditor = true
}
}
// 显示图片编辑对话框
if (showImageEditor && selectedImageUri != null) {
ImageEditorDialog(
imageUri = selectedImageUri!!,
onDismiss = {
showImageEditor = false
selectedImageUri = null
},
onConfirm = { transformedUri ->
context.saveAndApplyCustomBackground(transformedUri)
isCustomBackgroundEnabled = true
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
saveCardConfig(context)
showImageEditor = false
selectedImageUri = null
// 显示成功提示
Toast.makeText(
context,
context.getString(R.string.background_set_success),
Toast.LENGTH_SHORT
).show()
}
)
}
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlphaUse = CardConfig.cardAlpha
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.more_settings)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlphaUse),
scrolledContainerColor = cardColor.copy(alpha = cardAlphaUse)),
navigationIcon = {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back))
}
},
scrollBehavior = scrollBehavior,
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// 外观设置部分
SectionHeader(
title = stringResource(R.string.appearance_settings),
expanded = isAppearanceExpanded,
onToggle = { isAppearanceExpanded = !isAppearanceExpanded }
)
AnimatedVisibility(
visible = isAppearanceExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Surface(
shape = RoundedCornerShape(16.dp),
tonalElevation = 1.dp,
modifier = Modifier.padding(bottom = 16.dp)
) {
Column {
// 主题模式
ListItem(
headlineContent = { Text(stringResource(R.string.theme_mode)) },
supportingContent = { Text(themeOptions[themeMode]) },
leadingContent = {
Icon(
Icons.Default.DarkMode,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
trailingContent = {
Icon(
Icons.AutoMirrored.Filled.NavigateNext,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
),
modifier = Modifier.clickable { showThemeModeDialog = true }
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 动态颜色开关
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SwitchItem(
icon = Icons.Filled.ColorLens,
title = stringResource(R.string.dynamic_color_title),
summary = stringResource(R.string.dynamic_color_summary),
checked = useDynamicColor
) { enabled ->
useDynamicColor = enabled
context.saveDynamicColorState(enabled)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
}
// 只在未启用动态颜色时显示主题色选择
AnimatedVisibility(
visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
ListItem(
headlineContent = { Text(stringResource(R.string.theme_color)) },
supportingContent = {
val currentThemeName = when (ThemeConfig.currentTheme) {
is ThemeColors.Default -> stringResource(R.string.color_default)
is ThemeColors.Green -> stringResource(R.string.color_green)
is ThemeColors.Purple -> stringResource(R.string.color_purple)
is ThemeColors.Orange -> stringResource(R.string.color_orange)
is ThemeColors.Pink -> stringResource(R.string.color_pink)
is ThemeColors.Gray -> stringResource(R.string.color_gray)
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
else -> stringResource(R.string.color_default)
}
Text(currentThemeName)
},
leadingContent = {
Icon(
Icons.Default.Palette,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
trailingContent = {
Icon(
Icons.AutoMirrored.Filled.NavigateNext,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
),
modifier = Modifier.clickable { showThemeColorDialog = true }
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
}
}
// 自定义背景开关
ListItem(
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
leadingContent = {
Icon(
Icons.Filled.Wallpaper,
contentDescription = null,
tint = if (isCustomBackgroundEnabled)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant
)
},
trailingContent = {
Switch(
checked = isCustomBackgroundEnabled,
onCheckedChange = { isChecked ->
if (isChecked) {
pickImageLauncher.launch("image/*")
} else {
context.saveCustomBackground(null)
isCustomBackgroundEnabled = false
CardConfig.cardElevation
CardConfig.cardAlpha = 1f
CardConfig.isCustomAlphaSet = false
CardConfig.isCustomBackgroundEnabled = false
saveCardConfig(context)
cardAlpha = 1f
// 重置其他相关设置
ThemeConfig.needsResetOnThemeChange = true
ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putBoolean(
"prevent_background_refresh",
false
)
}
Toast.makeText(
context,
context.getString(R.string.background_removed),
Toast.LENGTH_SHORT
).show()
}
}
)
},
colors = ListItemDefaults.colors(
containerColor = Color.Transparent
)
)
// 透明度 Slider
AnimatedVisibility(
visible = ThemeConfig.customBackgroundUri != null,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
modifier = Modifier.padding(horizontal = 32.dp)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(
Icons.Filled.Opacity,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.settings_card_alpha),
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${(cardAlpha * 100).roundToInt()}%",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Slider(
value = cardAlpha,
onValueChange = { newValue ->
cardAlpha = newValue
CardConfig.cardAlpha = newValue
CardConfig.isCustomAlphaSet = true
prefs.edit {
putBoolean("is_custom_alpha_set", true)
putFloat("card_alpha", newValue)
}
},
onValueChangeFinished = {
coroutineScope.launch(Dispatchers.IO) {
saveCardConfig(context)
}
},
valueRange = 0f..1f,
steps = 20,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.primary,
activeTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
}
}
}
}
// 自定义设置部分
SectionHeader(
title = stringResource(R.string.custom_settings),
expanded = isCustomizeExpanded,
onToggle = { isCustomizeExpanded = !isCustomizeExpanded }
)
AnimatedVisibility(
visible = isCustomizeExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Surface(
shape = RoundedCornerShape(16.dp),
tonalElevation = 1.dp,
modifier = Modifier.padding(bottom = 16.dp)
) {
Column {
// 添加简洁模式开关
SwitchItem(
icon = Icons.Filled.Brush,
title = stringResource(R.string.simple_mode),
summary = stringResource(R.string.simple_mode_summary),
checked = isSimpleMode
) {
onSimpleModeChange(it)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 隐藏内核部分版本号
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_kernel_kernelsu_version),
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
checked = isHideVersion
) {
onHideVersionChange(it)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 模块数量等信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_other_info),
summary = stringResource(R.string.hide_other_info_summary),
checked = isHideOtherInfo
) {
onHideOtherInfoChange(it)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// SuSFS 状态信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_susfs_status),
summary = stringResource(R.string.hide_susfs_status_summary),
checked = isHideSusfsStatus
) {
onHideSusfsStatusChange(it)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
// 隐藏链接信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_link_card),
summary = stringResource(R.string.hide_link_card_summary),
checked = isHideLinkCard
) {
onHideLinkCardChange(it)
}
}
}
}
// 高级设置部分
SectionHeader(
title = stringResource(R.string.advanced_settings),
expanded = isAdvancedExpanded,
onToggle = { isAdvancedExpanded = !isAdvancedExpanded }
)
AnimatedVisibility(
visible = isAdvancedExpanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Surface(
shape = RoundedCornerShape(16.dp),
tonalElevation = 1.dp,
modifier = Modifier.padding(bottom = 16.dp)
) {
Column {
// SELinux 开关
KsuIsValid {
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.selinux),
summary = if (selinuxEnabled)
stringResource(R.string.selinux_enabled) else
stringResource(R.string.selinux_disabled),
checked = selinuxEnabled
) { enabled ->
val command = if (enabled) "setenforce 1" else "setenforce 0"
Shell.getShell().newJob().add(command).exec().let { result ->
if (result.isSuccess) {
selinuxEnabled = enabled
// 显示成功提示
val message = if (enabled)
context.getString(R.string.selinux_enabled_toast)
else
context.getString(R.string.selinux_disabled_toast)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} else {
// 显示失败提示
Toast.makeText(
context,
context.getString(R.string.selinux_change_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
}
// SuSFS 配置(仅在支持时显示)
val suSFS = getSuSFS()
val isSUS_SU = getSuSFSFeatures()
if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
// 初始化时,默认启用
var isEnabled by rememberSaveable {
mutableStateOf(true) // 默认启用
}
// 在启动时检查状态
LaunchedEffect(Unit) {
// 如果当前模式不是2就强制启用
val currentMode = susfsSUS_SU_Mode()
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
if (currentMode != "2" && wasManuallyDisabled) {
susfsSUS_SU_2() // 强制切换到模式2
prefs.edit { putBoolean("enable_sus_su", true) }
}
isEnabled = currentMode == "2"
}
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.settings_susfs_toggle),
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
checked = isEnabled
) {
if (it) {
// 手动启用
susfsSUS_SU_2()
prefs.edit { putBoolean("enable_sus_su", true) }
Toast.makeText(
context,
context.getString(R.string.susfs_enabled),
Toast.LENGTH_SHORT
).show()
} else {
// 手动关闭
susfsSUS_SU_0()
prefs.edit { putBoolean("enable_sus_su", false) }
Toast.makeText(
context,
context.getString(R.string.susfs_disabled),
Toast.LENGTH_SHORT
).show()
}
isEnabled = it
}
}
}
}
}
}
}
// 主题模式选择对话框
if (showThemeModeDialog) {
AlertDialog(
onDismissRequest = { showThemeModeDialog = false },
title = { Text(stringResource(R.string.theme_mode)) },
text = {
Column {
themeOptions.forEachIndexed { index, option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
themeMode = index
val newThemeMode = when(index) {
0 -> null // 跟随系统
1 -> false // 浅色
2 -> true // 深色
else -> null
}
context.saveThemeMode(newThemeMode)
when (index) {
2 -> { // 深色
ThemeConfig.forceDarkMode = true
CardConfig.isUserDarkModeEnabled = true
CardConfig.isUserLightModeEnabled = false
if (!CardConfig.isCustomAlphaSet) {
CardConfig.cardAlpha = 1f
}
CardConfig.save(context)
}
1 -> { // 浅色
ThemeConfig.forceDarkMode = false
CardConfig.isUserLightModeEnabled = true
CardConfig.isUserDarkModeEnabled = false
if (!CardConfig.isCustomAlphaSet) {
CardConfig.cardAlpha = 1f
}
CardConfig.save(context)
}
0 -> { // 跟随系统
ThemeConfig.forceDarkMode = null
CardConfig.isUserLightModeEnabled = false
CardConfig.isUserDarkModeEnabled = false
CardConfig.save(context)
}
}
showThemeModeDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = themeMode == index,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(option)
}
}
}
},
confirmButton = {
TextButton(
onClick = { showThemeModeDialog = false }
) {
Text(stringResource(R.string.cancel))
}
}
)
}
// 主题色选择对话框
if (showThemeColorDialog) {
AlertDialog(
onDismissRequest = { showThemeColorDialog = false },
title = { Text(stringResource(R.string.choose_theme_color)) },
text = {
Column {
themeColorOptions.forEach { (name, theme) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
context.saveThemeColors(when (theme) {
ThemeColors.Default -> "default"
ThemeColors.Green -> "green"
ThemeColors.Purple -> "purple"
ThemeColors.Orange -> "orange"
ThemeColors.Pink -> "pink"
ThemeColors.Gray -> "gray"
ThemeColors.Yellow -> "yellow"
else -> "default"
})
showThemeColorDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = ThemeConfig.currentTheme::class == theme::class,
onClick = null
)
Spacer(modifier = Modifier.width(12.dp))
Box(
modifier = Modifier
.size(24.dp)
.clip(CircleShape)
.background(theme.Primary)
)
Spacer(modifier = Modifier.width(12.dp))
Text(name)
}
}
}
},
confirmButton = {
TextButton(
onClick = { showThemeColorDialog = false }
) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@Composable
fun SectionHeader(
title: String,
expanded: Boolean,
onToggle: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle() }
.padding(vertical = 12.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (expanded)
stringResource(R.string.collapse)
else
stringResource(R.string.expand),
tint = MaterialTheme.colorScheme.primary
)
}
}

View File

@@ -0,0 +1,722 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
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.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.core.content.edit
import com.maxkeppeker.sheets.core.models.base.IconSource
import com.maxkeppeler.sheets.list.models.ListOption
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.*
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SettingScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
Scaffold(
topBar = {
TopBar(
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
val aboutDialog = rememberCustomDialog {
AboutDialog(it)
}
val loadingDialog = rememberLoadingDialog()
// endregion
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
) {
// region 上下文与协程
val context = LocalContext.current
val scope = rememberCoroutineScope()
// endregion
// region 日志导出功能
val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
loadingDialog.show()
context.contentResolver.openOutputStream(uri)?.use { output ->
getBugreportFile(context).inputStream().use {
it.copyTo(output)
}
}
loadingDialog.hide()
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
}
}
// 配置
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.configuration),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// 配置文件模板入口
val profileTemplate = stringResource(id = R.string.settings_profile_template)
KsuIsValid {
SettingItem(
icon = Icons.Filled.Fence,
title = profileTemplate,
summary = stringResource(id = R.string.settings_profile_template_summary),
onClick = {
navigator.navigate(AppProfileTemplateScreenDestination)
}
)
}
// 卸载模块开关
var umountChecked by rememberSaveable {
mutableStateOf(Natives.isDefaultUmountModules())
}
KsuIsValid {
SwitchSettingItem(
icon = Icons.Filled.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked,
onCheckedChange = {
if (Natives.setDefaultUmountModules(it)) {
umountChecked = it
}
}
)
}
// SU 禁用开关(仅在兼容版本显示)
KsuIsValid {
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled())
}
SwitchSettingItem(
icon = Icons.Filled.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary),
checked = isSuDisabled,
onCheckedChange = { checked ->
val shouldEnable = !checked
if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = !shouldEnable
}
}
)
}
}
}
}
// 设置分组卡片 - 应用设置
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.app_settings),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
// 更新检查开关
var checkUpdate by rememberSaveable {
mutableStateOf(
prefs.getBoolean("check_update", true)
)
}
SwitchSettingItem(
icon = Icons.Filled.Update,
title = stringResource(id = R.string.settings_check_update),
summary = stringResource(id = R.string.settings_check_update_summary),
checked = checkUpdate,
onCheckedChange = {
prefs.edit {putBoolean("check_update", it) }
checkUpdate = it
}
)
// Web调试开关
var enableWebDebugging by rememberSaveable {
mutableStateOf(
prefs.getBoolean("enable_web_debugging", false)
)
}
KsuIsValid {
SwitchSettingItem(
icon = Icons.Filled.DeveloperMode,
title = stringResource(id = R.string.enable_web_debugging),
summary = stringResource(id = R.string.enable_web_debugging_summary),
checked = enableWebDebugging,
onCheckedChange = {
prefs.edit { putBoolean("enable_web_debugging", it) }
enableWebDebugging = it
}
)
}
// 更多设置
SettingItem(
icon = Icons.Filled.Settings,
title = stringResource(id = R.string.more_settings),
summary = stringResource(id = R.string.more_settings),
onClick = {
navigator.navigate(MoreSettingsScreenDestination)
}
)
}
}
// 设置分组卡片 - 工具
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.tools),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
var showBottomsheet by remember { mutableStateOf(false) }
SettingItem(
icon = Icons.Filled.BugReport,
title = stringResource(id = R.string.send_log),
onClick = {
showBottomsheet = true
}
)
if (showBottomsheet) {
ModalBottomSheet(
onDismissRequest = { showBottomsheet = false },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
LogActionButton(
icon = Icons.Filled.Save,
text = stringResource(R.string.save_log),
onClick = {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
showBottomsheet = false
}
)
LogActionButton(
icon = Icons.Filled.Share,
text = stringResource(R.string.send_log),
onClick = {
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)
)
)
showBottomsheet = false
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) {
UninstallItem(navigator) {
loadingDialog.withLoading(it)
}
}
}
}
// 设置分组卡片 - 关于
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
SettingItem(
icon = Icons.Filled.Info,
title = stringResource(R.string.about),
onClick = {
aboutDialog.show()
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
@Composable
fun LogActionButton(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable(onClick = onClick)
.padding(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
) {
Icon(
imageVector = icon,
contentDescription = text,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
@Composable
fun SettingItem(
icon: ImageVector,
title: String,
summary: String? = null,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Icon(
imageVector = Icons.Filled.ChevronRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
@Composable
fun SwitchSettingItem(
icon: ImageVector,
title: String,
summary: String? = null,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange(!checked) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
if (summary != null) {
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
)
}
}
@Composable
fun UninstallItem(
navigator: DestinationsNavigator,
withLoading: suspend (suspend () -> Unit) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val uninstallConfirmDialog = rememberConfirmDialog()
val showTodo = {
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
}
val uninstallDialog = rememberUninstallDialog { uninstallType ->
scope.launch {
val result = uninstallConfirmDialog.awaitConfirm(
title = context.getString(uninstallType.title),
content = context.getString(uninstallType.message)
)
if (result == ConfirmResult.Confirmed) {
withLoading {
when (uninstallType) {
UninstallType.TEMPORARY -> showTodo()
UninstallType.PERMANENT -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashUninstall)
)
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashRestore)
)
UninstallType.NONE -> Unit
}
}
}
}
}
SettingItem(
icon = Icons.Filled.Delete,
title = stringResource(id = R.string.settings_uninstall),
onClick = {
uninstallDialog.show()
}
)
}
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
TEMPORARY(
R.string.settings_uninstall_temporary,
R.string.settings_uninstall_temporary_message,
Icons.Filled.Delete
),
PERMANENT(
R.string.settings_uninstall_permanent,
R.string.settings_uninstall_permanent_message,
Icons.Filled.DeleteForever
),
RESTORE_STOCK_IMAGE(
R.string.settings_restore_stock_image,
R.string.settings_restore_stock_image_message,
Icons.AutoMirrored.Filled.Undo
),
NONE(0, 0, Icons.Filled.Delete)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val options = listOf(
// UninstallType.TEMPORARY,
UninstallType.PERMANENT,
UninstallType.RESTORE_STOCK_IMAGE
)
val listOptions = options.map {
ListOption(
titleText = stringResource(it.title),
subtitleText = if (it.message != 0) stringResource(it.message) else null,
icon = IconSource(it.icon)
)
}
var selection = UninstallType.NONE
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
AlertDialog(
onDismissRequest = {
dismiss()
},
title = {
Text(
text = stringResource(R.string.settings_uninstall),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
)
},
text = {
Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
listOptions.forEachIndexed { index, option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable {
selection = options[index]
}
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = options[index].icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column {
Text(
text = option.titleText,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
option.subtitleText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
},
confirmButton = {
TextButton(
onClick = {
if (selection != UninstallType.NONE) {
onSelected(selection)
}
dismiss()
}
) {
Text(
text = stringResource(android.R.string.ok),
color = MaterialTheme.colorScheme.primary
)
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
}
) {
Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
}
},
containerColor = cardColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val systemIsDark = isSystemInDarkTheme()
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
cardAlpha
} else {
if (systemIsDark) 0.8f else 1f
}
TopAppBar(
title = {
Text(
text = stringResource(R.string.settings),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}

View File

@@ -0,0 +1,589 @@
package com.sukisu.ultra.ui.screen
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.sukisu.ultra.R
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.component.SearchAppBar
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.util.ModuleModify
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SuperUserScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<SuperUserViewModel>()
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val listState = rememberLazyListState()
val context = LocalContext.current
val snackBarHostState = remember { SnackbarHostState() }
// 添加备份和还原启动器
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
LaunchedEffect(key1 = navigator) {
viewModel.search = ""
if (viewModel.appList.isEmpty()) {
viewModel.fetchAppList()
}
}
LaunchedEffect(viewModel.search) {
if (viewModel.search.isEmpty()) {
listState.scrollToItem(0)
}
}
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.superuser)) },
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),
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
DropdownMenuItem(
text = { Text(stringResource(R.string.refresh)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
)
},
onClick = {
scope.launch {
viewModel.fetchAppList()
}
showDropdown = false
}
)
DropdownMenuItem(
text = {
Text(
if (viewModel.showSystemApps) {
stringResource(R.string.hide_system_apps)
} else {
stringResource(R.string.show_system_apps)
}
)
},
leadingIcon = {
Icon(
imageVector = if (viewModel.showSystemApps)
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
contentDescription = null,
)
},
onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false
}
)
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem(
text = { Text(stringResource(R.string.backup_allowlist)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = null,
)
},
onClick = {
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
showDropdown = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.restore_allowlist)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.RestoreFromTrash,
contentDescription = null,
)
},
onClick = {
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
showDropdown = false
}
)
}
}
},
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(snackBarHostState) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
bottomBar = {
AnimatedVisibility(
visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(),
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it })
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
tonalElevation = cardElevation,
shadowElevation = cardElevation
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
OutlinedButton(
onClick = {
viewModel.selectedApps = emptySet()
viewModel.showBatchActions = false
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(android.R.string.cancel))
}
Button(
onClick = {
scope.launch {
viewModel.updateBatchPermissions(true)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(R.string.batch_authorization))
}
Button(
onClick = {
scope.launch {
viewModel.updateBatchPermissions(false)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Icon(
imageVector = Icons.Filled.Block,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(stringResource(R.string.batch_cancel_authorization))
}
}
}
}
}
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier.padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
contentPadding = PaddingValues(
top = 8.dp,
bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp
)
) {
// 获取分组后的应用列表
val rootApps = viewModel.appList.filter { it.allowSu }
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
// 显示ROOT权限应用组
if (rootApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.apps_with_root))
}
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
// 显示自定义配置应用组
if (customApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
}
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
// 显示其他应用组
if (otherApps.isNotEmpty()) {
item {
GroupHeader(title = stringResource(R.string.other_apps))
}
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
// 当没有应用显示时显示空状态
if (viewModel.appList.isEmpty()) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Apps,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
modifier = Modifier
.size(96.dp)
.padding(bottom = 16.dp)
)
Text(
text = stringResource(R.string.no_apps_found),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
@Composable
fun GroupHeader(title: String) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = title,
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun AppItem(
app: SuperUserViewModel.AppInfo,
isSelected: Boolean,
onToggleSelection: () -> Unit,
onSwitchChange: (Boolean) -> Unit,
onClick: () -> Unit,
onLongClick: () -> Unit,
viewModel: SuperUserViewModel
) {
val cardColor = if (app.allowSu)
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
else if (app.hasCustomProfile)
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
else
MaterialTheme.colorScheme.surfaceContainerLow
Card(
colors = CardDefaults.cardColors(containerColor = cardColor),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = 0.dp,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
.then(
if (isSelected)
Modifier.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = MaterialTheme.shapes.medium
)
else
Modifier
)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { onLongClick() },
onTap = { onClick() }
)
}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(app.packageInfo)
.crossfade(true)
.build(),
contentDescription = app.label,
modifier = Modifier
.padding(end = 16.dp)
.size(48.dp)
.clip(MaterialTheme.shapes.small)
)
Column(
modifier = Modifier
.weight(1f)
.padding(end = 8.dp)
) {
Text(
text = app.label,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
Text(
text = app.packageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
FlowRow(
modifier = Modifier.padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (app.allowSu) {
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
}
if (Natives.uidShouldUmount(app.uid)) {
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
}
if (app.hasCustomProfile) {
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
}
}
}
if (!viewModel.showBatchActions) {
Switch(
checked = app.allowSu,
onCheckedChange = onSwitchChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
)
} else {
Checkbox(
checked = isSelected,
onCheckedChange = { onToggleSelection() },
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
}
}
}
}
@Composable
fun LabelText(label: String, backgroundColor: Color) {
Box(
modifier = Modifier
.padding(top = 2.dp, end = 2.dp)
.background(
backgroundColor,
shape = RoundedCornerShape(4.dp)
)
.clip(RoundedCornerShape(4.dp))
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
style = TextStyle(
fontSize = 10.sp,
color = Color.White,
fontWeight = FontWeight.Medium
)
)
}
}

View File

@@ -1,5 +1,7 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -31,6 +33,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
@@ -44,11 +47,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
@@ -59,9 +61,9 @@ import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.getOr
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import zako.zako.zako.R
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.ui.viewmodel.TemplateViewModel
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
/**
* @author weishu
@@ -78,11 +80,6 @@ fun AppProfileTemplateScreen(
val viewModel = viewModel<TemplateViewModel>()
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
LaunchedEffect(Unit) {
if (viewModel.templateList.isEmpty()) {
@@ -97,10 +94,13 @@ fun AppProfileTemplateScreen(
}
}
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
Scaffold(
topBar = {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val clipboardManager = context.getSystemService<ClipboardManager>()
val showToast = fun(msg: String) {
scope.launch(Dispatchers.Main) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
@@ -108,24 +108,28 @@ fun AppProfileTemplateScreen(
}
TopBar(
onBack = dropUnlessResumed { navigator.popBackStack() },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColorUse.copy(alpha = cardAlpha),
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
),
onSync = {
scope.launch { viewModel.fetchTemplates(true) }
},
onImport = {
clipboardManager.getText()?.text?.let {
if (it.isEmpty()) {
scope.launch {
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
if (clipboardText.isNullOrEmpty()) {
showToast(context.getString(R.string.app_profile_template_import_empty))
return@let
}
scope.launch {
viewModel.importTemplates(
it, {
showToast(context.getString(R.string.app_profile_template_import_success))
viewModel.fetchTemplates(false)
},
showToast
)
return@launch
}
viewModel.importTemplates(
clipboardText,
{
showToast(context.getString(R.string.app_profile_template_import_success))
viewModel.fetchTemplates(false)
},
showToast
)
}
},
onExport = {
@@ -134,8 +138,8 @@ fun AppProfileTemplateScreen(
{
showToast(context.getString(R.string.app_profile_template_export_empty))
}
) {
clipboardManager.setText(AnnotatedString(it))
) { text ->
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
}
}
},
@@ -154,7 +158,6 @@ fun AppProfileTemplateScreen(
},
icon = { Icon(Icons.Filled.Add, null) },
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
containerColor = cardColor.copy(alpha = 1f),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
},
@@ -204,17 +207,17 @@ private fun TemplateItem(
)
Text(template.description)
FlowRow {
LabelText(label = "UID: ${template.uid}")
LabelText(label = "GID: ${template.gid}")
LabelText(label = template.context)
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
if (template.local) {
LabelText(label = "local")
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
} else {
LabelText(label = "remote")
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
}
}
}
},
}
)
}
@@ -225,12 +228,20 @@ private fun TopBar(
onSync: () -> Unit = {},
onImport: () -> Unit = {},
onExport: () -> Unit = {},
colors: TopAppBarColors,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val cardColor = MaterialTheme.colorScheme.surfaceVariant
val cardAlpha = CardConfig.cardAlpha
TopAppBar(
title = {
Text(stringResource(R.string.settings_profile_template))
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
navigationIcon = {
IconButton(
onClick = onBack

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.screen
package com.sukisu.ultra.ui.screen
import android.widget.Toast
import androidx.activity.compose.BackHandler
@@ -47,14 +47,14 @@ import androidx.compose.ui.text.input.KeyboardType
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.result.ResultBackNavigator
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.ui.component.profile.RootProfileConfig
import zako.zako.zako.ui.util.deleteAppProfileTemplate
import zako.zako.zako.ui.util.getAppProfileTemplate
import zako.zako.zako.ui.util.setAppProfileTemplate
import zako.zako.zako.ui.viewmodel.TemplateViewModel
import zako.zako.zako.ui.viewmodel.toJSON
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
import com.sukisu.ultra.ui.util.deleteAppProfileTemplate
import com.sukisu.ultra.ui.util.getAppProfileTemplate
import com.sukisu.ultra.ui.util.setAppProfileTemplate
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
import com.sukisu.ultra.ui.viewmodel.toJSON
import androidx.lifecycle.compose.dropUnlessResumed
/**

View File

@@ -0,0 +1,107 @@
package com.sukisu.ultra.ui.theme
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
object CardConfig {
val settingElevation: Dp = 4.dp
val customBackgroundElevation: Dp = 0.dp
var cardAlpha by mutableStateOf(1f)
var cardElevation by mutableStateOf(settingElevation)
var isShadowEnabled by mutableStateOf(true)
var isCustomAlphaSet by mutableStateOf(false)
var isUserDarkModeEnabled by mutableStateOf(false)
var isUserLightModeEnabled by mutableStateOf(false)
var isCustomBackgroundEnabled by mutableStateOf(false)
/**
* 保存卡片配置到SharedPreferences
*/
fun save(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().apply {
putFloat("card_alpha", cardAlpha)
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
putBoolean("is_shadow_enabled", isShadowEnabled)
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
apply()
}
}
/**
* 从SharedPreferences加载卡片配置
*/
fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 1f)
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
updateShadowEnabled(isShadowEnabled)
}
/**
* 更新阴影启用状态
*/
fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
customBackgroundElevation
} else if (enabled) {
settingElevation
} else {
customBackgroundElevation
}
}
/**
* 设置深色模式默认值
*/
fun setDarkModeDefaults() {
if (!isCustomAlphaSet) {
cardAlpha = 1f
}
updateShadowEnabled(isShadowEnabled)
}
}
/**
* 获取卡片颜色配置
*/
@Composable
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = determineContentColor(originalColor)
)
/**
* 根据背景颜色、主题模式和用户设置确定内容颜色
*/
@Composable
private fun determineContentColor(originalColor: Color): Color {
val isDarkTheme = isSystemInDarkTheme()
if (ThemeConfig.isThemeChanging) {
return if (isDarkTheme) Color.White else Color.Black
}
return when {
CardConfig.isUserLightModeEnabled -> Color.Black
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
isDarkTheme -> Color.White
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
}
}

View File

@@ -0,0 +1,273 @@
package com.sukisu.ultra.ui.theme
import androidx.compose.ui.graphics.Color
sealed class ThemeColors {
abstract val Primary: Color
abstract val Secondary: Color
abstract val Tertiary: Color
abstract val OnPrimary: Color
abstract val OnSecondary: Color
abstract val OnTertiary: Color
abstract val PrimaryContainer: Color
abstract val SecondaryContainer: Color
abstract val TertiaryContainer: Color
abstract val OnPrimaryContainer: Color
abstract val OnSecondaryContainer: Color
abstract val OnTertiaryContainer: Color
abstract val ButtonContrast: Color
// 表面颜色
abstract val Surface: Color
abstract val SurfaceVariant: Color
abstract val OnSurface: Color
abstract val OnSurfaceVariant: Color
// 错误状态颜色
abstract val Error: Color
abstract val OnError: Color
abstract val ErrorContainer: Color
abstract val OnErrorContainer: Color
// 边框和背景色
abstract val Outline: Color
abstract val OutlineVariant: Color
abstract val Background: Color
abstract val OnBackground: Color
// 默认主题 (蓝色)
object Default : ThemeColors() {
override val Primary = Color(0xFF2196F3)
override val Secondary = Color(0xFF64B5F6)
override val Tertiary = Color(0xFF0D47A1)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFD6EAFF)
override val SecondaryContainer = Color(0xFFE3F2FD)
override val TertiaryContainer = Color(0xFFCFD8DC)
override val OnPrimaryContainer = Color(0xFF0A3049)
override val OnSecondaryContainer = Color(0xFF0D3C61)
override val OnTertiaryContainer = Color(0xFF071D41)
override val ButtonContrast = Color(0xFF2196F3)
override val Surface = Color(0xFFF5F9FF)
override val SurfaceVariant = Color(0xFFEDF5FE)
override val OnSurface = Color(0xFF1A1C1E)
override val OnSurfaceVariant = Color(0xFF42474E)
override val Error = Color(0xFFB00020)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFDE7E9)
override val OnErrorContainer = Color(0xFF410008)
override val Outline = Color(0xFFBAC3CF)
override val OutlineVariant = Color(0xFFDFE3EB)
override val Background = Color(0xFFFAFCFF)
override val OnBackground = Color(0xFF1A1C1E)
}
// 绿色主题
object Green : ThemeColors() {
override val Primary = Color(0xFF43A047)
override val Secondary = Color(0xFF66BB6A)
override val Tertiary = Color(0xFF1B5E20)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFD8EFDB)
override val SecondaryContainer = Color(0xFFE8F5E9)
override val TertiaryContainer = Color(0xFFB9F6CA)
override val OnPrimaryContainer = Color(0xFF0A280D)
override val OnSecondaryContainer = Color(0xFF0E2912)
override val OnTertiaryContainer = Color(0xFF051B07)
override val ButtonContrast = Color(0xFF43A047)
override val Surface = Color(0xFFF6FBF6)
override val SurfaceVariant = Color(0xFFEDF7EE)
override val OnSurface = Color(0xFF191C19)
override val OnSurfaceVariant = Color(0xFF414941)
override val Error = Color(0xFFC62828)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFF8D7DA)
override val OnErrorContainer = Color(0xFF4A0808)
override val Outline = Color(0xFFBDC9BF)
override val OutlineVariant = Color(0xFFDDE6DE)
override val Background = Color(0xFFFBFDFB)
override val OnBackground = Color(0xFF191C19)
}
// 紫色主题
object Purple : ThemeColors() {
override val Primary = Color(0xFF9C27B0)
override val Secondary = Color(0xFFBA68C8)
override val Tertiary = Color(0xFF6A1B9A)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFF3D8F8)
override val SecondaryContainer = Color(0xFFF5E9F7)
override val TertiaryContainer = Color(0xFFE1BEE7)
override val OnPrimaryContainer = Color(0xFF2A0934)
override val OnSecondaryContainer = Color(0xFF3C0F50)
override val OnTertiaryContainer = Color(0xFF1D0830)
override val ButtonContrast = Color(0xFF9C27B0)
override val Surface = Color(0xFFFCF6FF)
override val SurfaceVariant = Color(0xFFF5EEFA)
override val OnSurface = Color(0xFF1D1B1E)
override val OnSurfaceVariant = Color(0xFF49454E)
override val Error = Color(0xFFD50000)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFFDCD5)
override val OnErrorContainer = Color(0xFF480000)
override val Outline = Color(0xFFC9B9D0)
override val OutlineVariant = Color(0xFFE8DAED)
override val Background = Color(0xFFFFFBFF)
override val OnBackground = Color(0xFF1D1B1E)
}
// 橙色主题
object Orange : ThemeColors() {
override val Primary = Color(0xFFFF9800)
override val Secondary = Color(0xFFFFB74D)
override val Tertiary = Color(0xFFE65100)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFF000000)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFECCC)
override val SecondaryContainer = Color(0xFFFFF0D9)
override val TertiaryContainer = Color(0xFFFFD180)
override val OnPrimaryContainer = Color(0xFF351F00)
override val OnSecondaryContainer = Color(0xFF3D2800)
override val OnTertiaryContainer = Color(0xFF2E1500)
override val ButtonContrast = Color(0xFFFF9800)
override val Surface = Color(0xFFFFF8F3)
override val SurfaceVariant = Color(0xFFFFF0E6)
override val OnSurface = Color(0xFF1F1B16)
override val OnSurfaceVariant = Color(0xFF4E4639)
override val Error = Color(0xFFD32F2F)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFFDBC8)
override val OnErrorContainer = Color(0xFF490700)
override val Outline = Color(0xFFD6C3AD)
override val OutlineVariant = Color(0xFFEFDFCC)
override val Background = Color(0xFFFFFBFF)
override val OnBackground = Color(0xFF1F1B16)
}
// 粉色主题
object Pink : ThemeColors() {
override val Primary = Color(0xFFE91E63)
override val Secondary = Color(0xFFF06292)
override val Tertiary = Color(0xFF880E4F)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFCE4EC)
override val SecondaryContainer = Color(0xFFFCE4EC)
override val TertiaryContainer = Color(0xFFF8BBD0)
override val OnPrimaryContainer = Color(0xFF3B0819)
override val OnSecondaryContainer = Color(0xFF3B0819)
override val OnTertiaryContainer = Color(0xFF2B0516)
override val ButtonContrast = Color(0xFFE91E63)
override val Surface = Color(0xFFFFF7F9)
override val SurfaceVariant = Color(0xFFFCEEF2)
override val OnSurface = Color(0xFF201A1C)
override val OnSurfaceVariant = Color(0xFF534347)
override val Error = Color(0xFFB71C1C)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFFDAD6)
override val OnErrorContainer = Color(0xFF410002)
override val Outline = Color(0xFFD6BABF)
override val OutlineVariant = Color(0xFFEFDDE0)
override val Background = Color(0xFFFFFBFF)
override val OnBackground = Color(0xFF201A1C)
}
// 灰色主题
object Gray : ThemeColors() {
override val Primary = Color(0xFF607D8B)
override val Secondary = Color(0xFF90A4AE)
override val Tertiary = Color(0xFF455A64)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFECEFF1)
override val SecondaryContainer = Color(0xFFECEFF1)
override val TertiaryContainer = Color(0xFFCFD8DC)
override val OnPrimaryContainer = Color(0xFF1A2327)
override val OnSecondaryContainer = Color(0xFF1A2327)
override val OnTertiaryContainer = Color(0xFF121A1D)
override val ButtonContrast = Color(0xFF607D8B)
override val Surface = Color(0xFFF6F9FB)
override val SurfaceVariant = Color(0xFFEEF2F4)
override val OnSurface = Color(0xFF191C1E)
override val OnSurfaceVariant = Color(0xFF41484D)
override val Error = Color(0xFFC62828)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFFDAD6)
override val OnErrorContainer = Color(0xFF410002)
override val Outline = Color(0xFFBDC1C4)
override val OutlineVariant = Color(0xFFDDE1E3)
override val Background = Color(0xFFFBFCFE)
override val OnBackground = Color(0xFF191C1E)
}
// 黄色主题
object Yellow : ThemeColors() {
override val Primary = Color(0xFFFFC107)
override val Secondary = Color(0xFFFFD54F)
override val Tertiary = Color(0xFFFF8F00)
override val OnPrimary = Color(0xFF000000)
override val OnSecondary = Color(0xFF000000)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFF8E1)
override val SecondaryContainer = Color(0xFFFFF8E1)
override val TertiaryContainer = Color(0xFFFFECB3)
override val OnPrimaryContainer = Color(0xFF332A00)
override val OnSecondaryContainer = Color(0xFF332A00)
override val OnTertiaryContainer = Color(0xFF221200)
override val ButtonContrast = Color(0xFFFFC107)
override val Surface = Color(0xFFFFFAF3)
override val SurfaceVariant = Color(0xFFFFF7E6)
override val OnSurface = Color(0xFF1F1C17)
override val OnSurfaceVariant = Color(0xFF4E4A3C)
override val Error = Color(0xFFB71C1C)
override val OnError = Color(0xFFFFFFFF)
override val ErrorContainer = Color(0xFFFFDAD6)
override val OnErrorContainer = Color(0xFF410002)
override val Outline = Color(0xFFD1C8AF)
override val OutlineVariant = Color(0xFFEEE8D7)
override val Background = Color(0xFFFFFCF8)
override val OnBackground = Color(0xFF1F1C17)
}
companion object {
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
"green" -> Green
"purple" -> Purple
"orange" -> Orange
"pink" -> Pink
"gray" -> Gray
"yellow" -> Yellow
else -> Default
}
}
}

View File

@@ -0,0 +1,538 @@
package com.sukisu.ultra.ui.theme
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.zIndex
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import androidx.compose.foundation.background
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import androidx.core.content.edit
import androidx.core.net.toUri
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
/**
* 主题配置对象,管理应用的主题相关状态
*/
object ThemeConfig {
var customBackgroundUri by mutableStateOf<Uri?>(null)
var forceDarkMode by mutableStateOf<Boolean?>(null)
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
var useDynamicColor by mutableStateOf(false)
var backgroundImageLoaded by mutableStateOf(false)
var needsResetOnThemeChange by mutableStateOf(false)
var isThemeChanging by mutableStateOf(false)
var preventBackgroundRefresh by mutableStateOf(false)
private var lastDarkModeState: Boolean? = null
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
lastDarkModeState = currentDarkMode
return isChanged
}
fun resetBackgroundState() {
if (!preventBackgroundRefresh) {
backgroundImageLoaded = false
}
isThemeChanging = true
}
}
/**
* 应用主题
*/
@Composable
fun KernelSUTheme(
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> isSystemInDarkTheme()
},
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val systemIsDark = isSystemInDarkTheme()
// 检测系统主题变化并保存状态
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
LaunchedEffect(systemIsDark, themeChanged) {
if (ThemeConfig.forceDarkMode == null && themeChanged) {
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
ThemeConfig.resetBackgroundState()
if (!ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
}
CardConfig.apply {
load(context)
if (!isCustomAlphaSet) {
cardAlpha = if (systemIsDark) 0.50f else 1f
}
save(context)
}
}
}
// 初始加载配置
LaunchedEffect(Unit) {
context.loadThemeMode()
context.loadThemeColors()
context.loadDynamicColorState()
CardConfig.load(context)
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
ThemeConfig.backgroundImageLoaded = false
}
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", true)
}
// 创建颜色方案
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
// 根据暗色模式和自定义背景调整卡片配置
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
if (darkTheme && !dynamicColor) {
CardConfig.setDarkModeDefaults()
}
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
LaunchedEffect(ThemeConfig.customBackgroundUri) {
backgroundUri.value = ThemeConfig.customBackgroundUri
}
val bgImagePainter = backgroundUri.value?.let {
rememberAsyncImagePainter(
model = it,
onError = {
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
ThemeConfig.customBackgroundUri = null
context.saveCustomBackground(null)
},
onSuccess = {
Log.d("ThemeSystem", "背景图加载成功")
ThemeConfig.backgroundImageLoaded = true
ThemeConfig.isThemeChanging = false
ThemeConfig.preventBackgroundRefresh = true
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { putBoolean("prevent_background_refresh", true) }
}
)
}
val transition = updateTransition(
targetState = ThemeConfig.backgroundImageLoaded,
label = "bgTransition"
)
val bgAlpha by transition.animateFloat(
label = "bgAlpha",
transitionSpec = {
spring(
dampingRatio = 0.8f,
stiffness = 300f
)
}
) { loaded -> if (loaded) 1f else 0f }
DisposableEffect(systemIsDark) {
onDispose {
if (ThemeConfig.isThemeChanging) {
ThemeConfig.isThemeChanging = false
}
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography
) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-2f)
.background(if (darkTheme) Color.Black else Color.White)
)
// 自定义背景层
backgroundUri.value?.let { uri ->
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
.alpha(bgAlpha)
) {
// 背景图片
bgImagePainter?.let { painter ->
Box(
modifier = Modifier
.fillMaxSize()
.paint(
painter = painter,
contentScale = ContentScale.Crop
)
.graphicsLayer {
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
}
)
}
// 亮度调节层
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) Color.Black.copy(alpha = 0.6f)
else Color.White.copy(alpha = 0.1f)
)
)
// 边缘渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) Color.Black.copy(alpha = 0.5f)
else Color.Black.copy(alpha = 0.2f)
),
radius = 1200f
)
)
)
}
}
// 内容层
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f)
) {
content()
}
}
}
}
/**
* 创建动态深色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicDarkColorScheme(context: Context) =
dynamicDarkColorScheme(context).copy(
background = Color.Transparent,
surface = Color.Transparent,
onBackground = Color.White,
onSurface = Color.White
)
/**
* 创建动态浅色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S)
@Composable
private fun createDynamicLightColorScheme(context: Context) =
dynamicLightColorScheme(context).copy(
background = Color.Transparent,
surface = Color.Transparent
)
/**
* 创建深色颜色方案
*/
@Composable
private fun createDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
onPrimary = Color.White,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
onPrimaryContainer = Color.White,
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
onSecondary = Color.White,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
onSecondaryContainer = Color.White,
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
onTertiary = Color.White,
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
onTertiaryContainer = Color.White,
background = Color.Transparent,
surface = Color.Transparent,
onBackground = Color.White,
onSurface = Color.White,
surfaceVariant = Color(0xFF2F2F2F),
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
outline = Color.White.copy(alpha = 0.12f),
outlineVariant = Color.White.copy(alpha = 0.12f),
error = ThemeConfig.currentTheme.Error,
onError = ThemeConfig.currentTheme.OnError,
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
onErrorContainer = Color.White
)
/**
* 创建浅色颜色方案
*/
@Composable
private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.Primary,
onPrimary = ThemeConfig.currentTheme.OnPrimary,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
secondary = ThemeConfig.currentTheme.Secondary,
onSecondary = ThemeConfig.currentTheme.OnSecondary,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
tertiary = ThemeConfig.currentTheme.Tertiary,
onTertiary = ThemeConfig.currentTheme.OnTertiary,
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
background = Color.Transparent,
surface = Color.Transparent,
onBackground = Color.Black.copy(alpha = 0.87f),
onSurface = Color.Black.copy(alpha = 0.87f),
surfaceVariant = Color(0xFFF5F5F5),
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
outline = Color.Black.copy(alpha = 0.12f),
outlineVariant = Color.Black.copy(alpha = 0.12f),
error = ThemeConfig.currentTheme.Error,
onError = ThemeConfig.currentTheme.OnError,
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
)
/**
* 复制图片到应用内部存储并提升持久性
*/
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
return try {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background.jpg"
val file = File(filesDir, fileName)
val backupFile = File(filesDir, "${fileName}.backup")
val outputStream = FileOutputStream(backupFile)
val buffer = ByteArray(4 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
outputStream.flush()
outputStream.close()
inputStream.close()
if (file.exists()) {
file.delete()
}
backupFile.renameTo(file)
Uri.fromFile(file)
} catch (e: Exception) {
Log.e("ImageCopy", "复制图片失败: ${e.message}")
null
}
}
/**
* 保存并应用自定义背景
*/
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
val finalUri = if (transformation != null) {
saveTransformedBackground(uri, transformation)
} else {
copyImageToInternalStorage(uri)
}
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", finalUri?.toString())
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
ThemeConfig.customBackgroundUri = finalUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
}
/**
* 保存自定义背景
*/
fun Context.saveCustomBackground(uri: Uri?) {
val newUri = uri?.let { copyImageToInternalStorage(it) }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", newUri?.toString())
if (uri == null) {
// 如果清除背景,也重置阻止刷新标志
putBoolean("prevent_background_refresh", false)
} else {
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
}
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
if (uri != null) {
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
}
}
/**
* 加载自定义背景
*/
fun Context.loadCustomBackground() {
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
val newUri = uriString?.toUri()
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
}
}
/**
* 保存主题模式
*/
fun Context.saveThemeMode(forceDark: Boolean?) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString(
"theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
}
)
}
ThemeConfig.forceDarkMode = forceDark
ThemeConfig.needsResetOnThemeChange = forceDark == null
}
/**
* 加载主题模式
*/
fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) {
"dark" -> true
"light" -> false
else -> null
}
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
}
/**
* 保存主题颜色
*/
fun Context.saveThemeColors(themeName: String) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
}
/**
* 加载主题颜色
*/
fun Context.loadThemeColors() {
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_colors", "default")
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
}
/**
* 保存动态颜色状态
*/
fun Context.saveDynamicColorState(enabled: Boolean) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
/**
* 加载动态颜色状态
*/
fun Context.loadDynamicColorState() {
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled
}

View File

@@ -0,0 +1,108 @@
package com.sukisu.ultra.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
// 大标题
displayLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
// 标题
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// 标题栏
titleLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
// 主体文字
bodyLarge = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
// 标签
labelLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)

View File

@@ -0,0 +1,110 @@
package com.sukisu.ultra.ui.util
import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.net.Uri
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import androidx.core.graphics.createBitmap
data class BackgroundTransformation(
val scale: Float = 1f,
val offsetX: Float = 0f,
val offsetY: Float = 0f
)
fun Context.getImageBitmap(uri: Uri): Bitmap? {
return try {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
bitmap
} catch (e: Exception) {
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
null
}
}
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
val width = bitmap.width
val height = bitmap.height
// 创建与屏幕比例相同的目标位图
val displayMetrics = resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val screenHeight = displayMetrics.heightPixels
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
// 计算目标宽高
val targetWidth: Int
val targetHeight: Int
if (width.toFloat() / height.toFloat() > screenRatio) {
targetHeight = height
targetWidth = (height / screenRatio).toInt()
} else {
targetWidth = width
targetHeight = (width * screenRatio).toInt()
}
// 创建与目标相同大小的位图
val scaledBitmap = createBitmap(targetWidth, targetHeight)
val canvas = Canvas(scaledBitmap)
val matrix = Matrix()
// 确保缩放值有效
val safeScale = maxOf(0.1f, transformation.scale)
matrix.postScale(safeScale, safeScale)
// 计算偏移量,确保不会出现负最大值的问题
val widthDiff = (bitmap.width * safeScale - targetWidth)
val heightDiff = (bitmap.height * safeScale - targetHeight)
// 安全计算偏移量边界
val maxOffsetX = maxOf(0f, widthDiff / 2)
val maxOffsetY = maxOf(0f, heightDiff / 2)
// 限制偏移范围
val safeOffsetX = if (maxOffsetX > 0)
transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
val safeOffsetY = if (maxOffsetY > 0)
transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
// 应用偏移量到矩阵
val translationX = -widthDiff / 2 + safeOffsetX
val translationY = -heightDiff / 2 + safeOffsetY
matrix.postTranslate(translationX, translationY)
// 将原始位图绘制到新位图上
canvas.drawBitmap(bitmap, matrix, null)
return scaledBitmap
}
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
try {
val bitmap = getImageBitmap(uri) ?: return null
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
val fileName = "custom_background_transformed.jpg"
val file = File(filesDir, fileName)
val outputStream = FileOutputStream(file)
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
outputStream.flush()
outputStream.close()
return Uri.fromFile(file)
} catch (e: Exception) {
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
return null
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import android.annotation.SuppressLint
import android.app.DownloadManager
@@ -12,7 +12,8 @@ import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.core.content.ContextCompat
import zako.zako.zako.ui.util.module.LatestVersionInfo
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import androidx.core.net.toUri
/**
* @author weishu
@@ -42,14 +43,14 @@ fun download(
onDownloading()
return
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
onDownloaded(Uri.parse(localUri))
onDownloaded(localUri.toUri())
return
}
}
}
}
val request = DownloadManager.Request(Uri.parse(url))
val request = DownloadManager.Request(url.toUri())
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
fileName
@@ -140,7 +141,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
val uri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
)
onDownloaded(Uri.parse(uri))
onDownloaded(uri.toUri())
}
}
}

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util;
package com.sukisu.ultra.ui.util;
/*
* Copyright (C) 2009 The Android Open Source Project
*

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material3.MaterialTheme

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import android.content.ContentResolver
import android.content.Context
@@ -16,9 +16,9 @@ import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import zako.zako.zako.BuildConfig
import zako.zako.zako.Natives
import zako.zako.zako.ksuApp
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import org.json.JSONArray
import java.io.File
@@ -436,7 +436,7 @@ fun restartApp(packageName: String) {
launchApp(packageName)
}
private fun getSuSFSDaemonPath(): String {
fun getSuSFSDaemonPath(): String {
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
}
@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
fun controlKpmModule(name: String, args: String? = null): Int {
val shell = getRootShell()
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
val result = runCmd(shell, cmd)
return result.trim().toIntOrNull() ?: -1
}

View File

@@ -1,11 +1,11 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import android.content.Context
import android.os.Build
import android.system.Os
import com.topjohnwu.superuser.ShellUtils
import zako.zako.zako.Natives
import zako.zako.zako.ui.screen.getManagerVersion
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.screen.getManagerVersion
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import android.app.AlertDialog
import android.content.Context
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.R
import com.sukisu.ultra.R
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader

View File

@@ -1,9 +1,9 @@
package zako.zako.zako.ui.util
package com.sukisu.ultra.ui.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.topjohnwu.superuser.Shell
import zako.zako.zako.R
import com.sukisu.ultra.R
@Composable
fun getSELinuxStatus(): String {

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.util.module
package com.sukisu.ultra.ui.util.module
data class LatestVersionInfo(
val versionCode : Int = 0,

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.viewmodel
package com.sukisu.ultra.ui.viewmodel
import android.util.Log
import androidx.compose.runtime.getValue
@@ -9,7 +9,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.ui.util.*
import com.sukisu.ultra.ui.util.*
class KpmViewModel : ViewModel() {
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
@@ -143,17 +143,6 @@ class KpmViewModel : ViewModel() {
return result
}
fun controlModule(moduleId: String, args: String? = null): Int {
return try {
val result = controlKpmModule(moduleId, args)
Log.d("KsuCli", "Control module $moduleId result: $result")
result
} catch (e: Exception) {
Log.e("KsuCli", "Failed to control module $moduleId", e)
-1
}
}
data class ModuleInfo(
val id: String,
val name: String,

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.viewmodel
package com.sukisu.ultra.ui.viewmodel
import android.os.SystemClock
import android.util.Log
@@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import zako.zako.zako.ui.util.HanziToPinyin
import zako.zako.zako.ui.util.listModules
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules
import org.json.JSONArray
import org.json.JSONObject
import java.text.Collator
@@ -40,13 +40,6 @@ class ModuleViewModel : ViewModel() {
val dirId: String, // real module id (dir name)
)
data class ModuleUpdateInfo(
val version: String,
val versionCode: Int,
val zipUrl: String,
val changelog: String,
)
var isRefreshing by mutableStateOf(false)
private set
var search by mutableStateOf("")

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.viewmodel
package com.sukisu.ultra.ui.viewmodel
import android.content.ComponentName
import android.content.Intent
@@ -18,12 +18,12 @@ import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import zako.zako.zako.IKsuInterface
import zako.zako.zako.Natives
import zako.zako.zako.ksuApp
import zako.zako.zako.ui.KsuService
import zako.zako.zako.ui.util.HanziToPinyin
import zako.zako.zako.ui.util.KsuCli
import com.sukisu.zako.IKsuInterface
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.KsuService
import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.KsuCli
import java.text.Collator
import java.util.*
import kotlin.coroutines.resume
@@ -68,9 +68,9 @@ class SuperUserViewModel : ViewModel() {
// 批量操作相关状态
var showBatchActions by mutableStateOf(false)
private set
internal set
var selectedApps by mutableStateOf<Set<String>>(emptySet())
private set
internal set
private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> {

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.viewmodel
package com.sukisu.ultra.ui.viewmodel
import android.os.Parcelable
import android.util.Log
@@ -10,12 +10,12 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import zako.zako.zako.Natives
import zako.zako.zako.profile.Capabilities
import zako.zako.zako.profile.Groups
import zako.zako.zako.ui.util.getAppProfileTemplate
import zako.zako.zako.ui.util.listAppProfileTemplates
import zako.zako.zako.ui.util.setAppProfileTemplate
import com.sukisu.ultra.Natives
import com.sukisu.ultra.profile.Capabilities
import com.sukisu.ultra.profile.Groups
import com.sukisu.ultra.ui.util.getAppProfileTemplate
import com.sukisu.ultra.ui.util.listAppProfileTemplates
import com.sukisu.ultra.ui.util.setAppProfileTemplate
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package zako.zako.zako.ui.webui;
package com.sukisu.ultra.ui.webui;
import java.net.URLConnection;

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.webui;
package com.sukisu.ultra.ui.webui;
import android.content.Context;
import android.util.Log;

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.webui
package com.sukisu.ultra.ui.webui
import android.annotation.SuppressLint
import android.app.ActivityManager
@@ -16,7 +16,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.webkit.WebViewAssetLoader
import com.topjohnwu.superuser.Shell
import zako.zako.zako.ui.util.createRootShell
import com.sukisu.ultra.ui.util.createRootShell
import java.io.File
@SuppressLint("SetJavaScriptEnabled")

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.ui.webui
package com.sukisu.ultra.ui.webui
import android.app.Activity
import android.content.Context
@@ -14,13 +14,13 @@ import androidx.core.view.WindowInsetsControllerCompat
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.UiThreadHandler
import zako.zako.zako.ui.util.createRootShell
import zako.zako.zako.ui.util.listModules
import zako.zako.zako.ui.util.withNewRootShell
import com.sukisu.ultra.ui.util.createRootShell
import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.withNewRootShell
import org.json.JSONArray
import org.json.JSONObject
import zako.zako.zako.ui.util.controlKpmModule
import zako.zako.zako.ui.util.listKpmModules
import com.sukisu.ultra.ui.util.controlKpmModule
import com.sukisu.ultra.ui.util.listKpmModules
import java.io.File
import java.util.concurrent.CompletableFuture

View File

@@ -1,4 +1,4 @@
package zako.zako.zako.utils
package com.sukisu.ultra.utils
import android.content.Context
import java.io.File

View File

@@ -1,8 +1,8 @@
package io.zako.zako;
package io.sukisu.ultra;
import java.util.ArrayList;
import zako.zako.zako.ui.util.KsuCli;
import com.sukisu.ultra.ui.util.KsuCli;
public class UltraShellHelper {
public static String runCmd(String cmds) {
@@ -19,7 +19,7 @@ public class UltraShellHelper {
}
public static boolean isPathExists(String path) {
return !runCmd("file " + path).contains("No such file or directory");
return runCmd("file " + path).contains("No such file or directory");
}
public static void CopyFileTo(String path, String target) {

View File

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

View File

@@ -1,14 +0,0 @@
package io.zako.zako;
import static zako.zako.zako.ui.util.KsuCliKt.getKpmmgrPath;
public class UltraToolInstall {
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
public static void tryToInstall() {
String kpmmgrPath = getKpmmgrPath();
if (!UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
}
}
}

View File

@@ -1,190 +0,0 @@
package zako.zako.zako.ui
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedContentTransitionScope
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.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.union
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import io.zako.zako.UltraToolInstall
import zako.zako.zako.Natives
import zako.zako.zako.ksuApp
import zako.zako.zako.ui.screen.BottomBarDestination
import zako.zako.zako.ui.theme.CardConfig
import zako.zako.zako.ui.theme.KernelSUTheme
import zako.zako.zako.ui.theme.loadCustomBackground
import zako.zako.zako.ui.theme.loadThemeMode
import zako.zako.zako.ui.util.LocalSnackbarHost
import zako.zako.zako.ui.util.getKpmVersion
import zako.zako.zako.ui.util.rootAvailable
import zako.zako.zako.ui.util.install
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
// 加载保存的背景设置
loadCustomBackground()
loadThemeMode()
CardConfig.load(applicationContext)
val isManager = Natives.becomeManager(ksuApp.packageName)
if (isManager) {
install()
UltraToolInstall.tryToInstall()
}
setContent {
KernelSUTheme {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
Scaffold(
bottomBar = { BottomBar(navController) },
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState,
) {
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root,
navController = navController,
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
get() = { fadeIn(animationSpec = tween(340)) }
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
get() = { fadeOut(animationSpec = tween(340)) }
}
)
}
}
}
}
}
}
@Composable
private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
val kpmVersion = getKpmVersion()
// 获取卡片颜色和透明度
val cardColor = MaterialTheme.colorScheme.secondaryContainer
val cardAlpha = CardConfig.cardAlpha
val cardElevation = CardConfig.cardElevation
NavigationBar(
tonalElevation = cardElevation, // 动态设置阴影
containerColor = cardColor.copy(alpha = cardAlpha),
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
)
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
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(stringResource(destination.label)) },
alwaysShowLabel = false,
colors = androidx.compose.material3.NavigationBarItemDefaults.colors(
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
}
} else {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
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(stringResource(destination.label)) },
alwaysShowLabel = false,
)
}
}
}
}

View File

@@ -1,32 +0,0 @@
package zako.zako.zako.ui.component
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun SwitchItem(
icon: ImageVector,
title: String,
summary: String,
checked: Boolean,
modifier: Modifier = Modifier,
onCheckedChange: (Boolean) -> Unit
) {
ListItem(
modifier = modifier,
leadingContent = { Icon(icon, contentDescription = null) },
headlineContent = { Text(title) },
supportingContent = { Text(summary) },
trailingContent = {
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
)
}

View File

@@ -1,393 +0,0 @@
package zako.zako.zako.ui.screen
import androidx.annotation.StringRes
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.width
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.AccountCircle
import androidx.compose.material.icons.filled.Android
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.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
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.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.ui.component.SwitchItem
import zako.zako.zako.ui.component.profile.AppProfileConfig
import zako.zako.zako.ui.component.profile.RootProfileConfig
import zako.zako.zako.ui.component.profile.TemplateConfig
import zako.zako.zako.ui.util.LocalSnackbarHost
import zako.zako.zako.ui.util.forceStopApp
import zako.zako.zako.ui.util.getSepolicy
import zako.zako.zako.ui.util.launchApp
import zako.zako.zako.ui.util.restartApp
import zako.zako.zako.ui.util.setSepolicy
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
/**
* @author weishu
* @date 2023/5/16.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun AppProfileScreen(
navigator: DestinationsNavigator,
appInfo: SuperUserViewModel.AppInfo,
) {
val context = LocalContext.current
val snackBarHost = LocalSnackbarHost.current
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val scope = rememberCoroutineScope()
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
val packageName = appInfo.packageName
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
if (initialProfile.allowSu) {
initialProfile.rules = getSepolicy(packageName)
}
var profile by rememberSaveable {
mutableStateOf(initialProfile)
}
Scaffold(
topBar = {
TopBar(
onBack = dropUnlessResumed { navigator.popBackStack() },
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
AppProfileInner(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState()),
packageName = appInfo.packageName,
appLabel = appInfo.label,
appIcon = {
AsyncImage(
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
contentDescription = appInfo.label,
modifier = Modifier
.padding(4.dp)
.width(48.dp)
.height(48.dp)
)
},
profile = profile,
onViewTemplate = {
getTemplateInfoById(it)?.let { info ->
navigator.navigate(TemplateEditorScreenDestination(info))
}
},
onManageTemplate = {
navigator.navigate(AppProfileTemplateScreenDestination())
},
onProfileChange = {
scope.launch {
if (it.allowSu) {
// sync with allowlist.c - forbid_system_uid
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
snackBarHost.showSnackbar(suNotAllowed)
return@launch
}
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
snackBarHost.showSnackbar(failToUpdateSepolicy)
return@launch
}
}
if (!Natives.setAppProfile(it)) {
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
} else {
profile = it
}
}
},
)
}
}
@Composable
private fun AppProfileInner(
modifier: Modifier = Modifier,
packageName: String,
appLabel: String,
appIcon: @Composable () -> Unit,
profile: Natives.Profile,
onViewTemplate: (id: String) -> Unit = {},
onManageTemplate: () -> Unit = {},
onProfileChange: (Natives.Profile) -> Unit,
) {
val isRootGranted = profile.allowSu
Column(modifier = modifier) {
AppMenuBox(packageName) {
ListItem(
headlineContent = { Text(appLabel) },
supportingContent = { Text(packageName) },
leadingContent = appIcon,
)
}
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.superuser),
checked = isRootGranted,
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
)
Crossfade(targetState = isRootGranted, label = "") { current ->
Column(
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
) {
if (current) {
val initialMode = if (profile.rootUseDefault) {
Mode.Default
} else if (profile.rootTemplate != null) {
Mode.Template
} else {
Mode.Custom
}
var mode by rememberSaveable {
mutableStateOf(initialMode)
}
ProfileBox(mode, true) {
// template mode shouldn't change profile here!
if (it == Mode.Default || it == Mode.Custom) {
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
}
mode = it
}
Crossfade(targetState = mode, label = "") { currentMode ->
if (currentMode == Mode.Template) {
TemplateConfig(
profile = profile,
onViewTemplate = onViewTemplate,
onManageTemplate = onManageTemplate,
onProfileChange = onProfileChange
)
} else if (mode == Mode.Custom) {
RootProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = onProfileChange
)
}
}
} else {
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
ProfileBox(mode, false) {
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
}
Crossfade(targetState = mode, label = "") { currentMode ->
val modifyEnabled = currentMode == Mode.Custom
AppProfileConfig(
fixedName = true,
profile = profile,
enabled = modifyEnabled,
onProfileChange = onProfileChange
)
}
}
}
}
}
}
private enum class Mode(@StringRes private val res: Int) {
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
val text: String
@Composable get() = stringResource(res)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
onBack: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TopAppBar(
title = {
Text(stringResource(R.string.profile))
},
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
},
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
@Composable
private fun ProfileBox(
mode: Mode,
hasTemplate: Boolean,
onModeChange: (Mode) -> Unit,
) {
ListItem(
headlineContent = { Text(stringResource(R.string.profile)) },
supportingContent = { Text(mode.text) },
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
)
HorizontalDivider(thickness = Dp.Hairline)
ListItem(headlineContent = {
Row(
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
) {
FilterChip(
selected = mode == Mode.Default,
label = { Text(stringResource(R.string.profile_default)) },
onClick = { onModeChange(Mode.Default) },
)
if (hasTemplate) {
FilterChip(
selected = mode == Mode.Template,
label = { Text(stringResource(R.string.profile_template)) },
onClick = { onModeChange(Mode.Template) },
)
}
FilterChip(
selected = mode == Mode.Custom,
label = { Text(stringResource(R.string.profile_custom)) },
onClick = { onModeChange(Mode.Custom) },
)
}
})
}
@Composable
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
var expanded by remember { mutableStateOf(false) }
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
val density = LocalDensity.current
BoxWithConstraints(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
touchPoint = it
expanded = true
}
}
) {
content()
val (offsetX, offsetY) = with(density) {
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
}
DropdownMenu(
expanded = expanded,
offset = DpOffset(offsetX, -offsetY),
onDismissRequest = {
expanded = false
},
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.launch_app)) },
onClick = {
expanded = false
launchApp(packageName)
},
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.force_stop_app)) },
onClick = {
expanded = false
forceStopApp(packageName)
},
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.restart_app)) },
onClick = {
expanded = false
restartApp(packageName)
},
)
}
}
}
@Preview
@Composable
private fun AppProfilePreview() {
var profile by remember { mutableStateOf(Natives.Profile("")) }
AppProfileInner(
packageName = "icu.nullptr.test",
appLabel = "Test",
appIcon = { Icon(Icons.Filled.Android, null) },
profile = profile,
onProfileChange = {
profile = it
},
)
}

View File

@@ -1,735 +0,0 @@
package zako.zako.zako.ui.screen
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.PowerManager
import android.system.Os
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.pm.PackageInfoCompat
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.SettingScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import zako.zako.zako.*
import zako.zako.zako.R
import zako.zako.zako.ui.component.rememberConfirmDialog
import zako.zako.zako.ui.util.*
import zako.zako.zako.ui.util.module.LatestVersionInfo
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import zako.zako.zako.ui.theme.getCardColors
import zako.zako.zako.ui.theme.getCardElevation
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.runtime.saveable.rememberSaveable
import zako.zako.zako.ui.theme.CardConfig
import androidx.core.content.edit
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
import kotlin.random.Random
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>(start = true)
@Composable
fun HomeScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current
var isSimpleMode by rememberSaveable { mutableStateOf(false) }
var isHideVersion by rememberSaveable { mutableStateOf(false) }
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
// 从 SharedPreferences 加载简洁模式状态
LaunchedEffect(Unit) {
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_simple_mode", false)
}
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
LaunchedEffect(Unit) {
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_version", false)
}
// 从 SharedPreferences 加载隐藏模块数量等信息开关状态
LaunchedEffect(Unit) {
isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_other_info", false)
}
// 从 SharedPreferences 加载隐藏 SuSFS 状态开关状态
LaunchedEffect(Unit) {
isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false)
}
val kernelVersion = getKernelVersion()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val isManager = Natives.becomeManager(ksuApp.packageName)
val deviceModel = getDeviceModel(context)
val ksuVersion = if (isManager) Natives.version else null
val managerVersion = getManagerVersion(context).second
val Zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel)
val isVersion = ksuVersion == 12777
val isManagerVersionValid = managerVersion > (ksuVersion ?: 0) + 33
val shouldTriggerRestart = Zako && kernelVersion.isGKI() && (isVersion || isManagerVersionValid)
LaunchedEffect(shouldTriggerRestart) {
if (shouldTriggerRestart) {
val random = Random.nextInt(0, 100)
if (random <= 95) {
reboot()
} else {
""
}
}
}
Scaffold(
topBar = {
TopBar(
kernelVersion,
onInstallClick = { navigator.navigate(InstallScreenDestination) },
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
if (shouldTriggerRestart) {
WarningCard(message = "zakozako")
return@Column
}
val isManager = Natives.becomeManager(ksuApp.packageName)
val ksuVersion = if (isManager) Natives.version else null
val lkmMode = ksuVersion?.let {
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
}
StatusCard(kernelVersion, ksuVersion, lkmMode) {
navigator.navigate(InstallScreenDestination)
}
if (isManager && Natives.requireNewKernel()) {
WarningCard(
stringResource(id = R.string.require_kernel_version).format(
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
)
)
}
if (ksuVersion != null && !rootAvailable()) {
WarningCard(
stringResource(id = R.string.grant_root_failed)
)
}
val checkUpdate =
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true)
if (checkUpdate) {
UpdateCard()
}
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
if (!isSimpleMode && clickCount < 3) {
AnimatedVisibility(
visible = clickCount < 3,
exit = shrinkVertically() + fadeOut()
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
clickCount++
prefs.edit { putInt("click_count", clickCount) }
}
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.using_mksu_manager),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
InfoCard()
if (!isSimpleMode) {
ContributionCard()
DonateCard()
LearnMoreCard()
}
Spacer(Modifier)
}
}
}
@Composable
fun UpdateCard() {
val context = LocalContext.current
val latestVersionInfo = LatestVersionInfo()
val newVersion by produceState(initialValue = latestVersionInfo) {
value = withContext(Dispatchers.IO) {
checkNewVersion()
}
}
val currentVersionCode = getManagerVersion(context).second
val newVersionCode = newVersion.versionCode
val newVersionUrl = newVersion.downloadUrl
val changelog = newVersion.changelog
Log.d("UpdateCard", "Current version code: $currentVersionCode")
Log.d("UpdateCard", "New version code: $newVersionCode")
val uriHandler = LocalUriHandler.current
val title = stringResource(id = R.string.module_changelog)
val updateText = stringResource(id = R.string.module_update)
AnimatedVisibility(
visible = newVersionCode > currentVersionCode,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard(
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
MaterialTheme.colorScheme.outlineVariant
) {
if (changelog.isEmpty()) {
uriHandler.openUri(newVersionUrl)
} else {
updateDialog.showConfirm(
title = title,
content = changelog,
markdown = true,
confirm = updateText
)
}
}
}
}
@Composable
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem(text = {
Text(stringResource(id))
}, onClick = {
reboot(reason)
})
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
kernelVersion: KernelVersion,
onInstallClick: () -> Unit,
onSettingsClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val cardColor = MaterialTheme.colorScheme.secondaryContainer
val cardAlpha = CardConfig.cardAlpha
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
actions = {
if (kernelVersion.isGKI()) {
IconButton(onClick = onInstallClick) {
Icon(Icons.Filled.Archive, stringResource(R.string.install))
}
}
var showDropdown by remember { mutableStateOf(false) }
IconButton(onClick = { showDropdown = true }) {
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot))
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }
) {
RebootDropdownItem(id = R.string.reboot)
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
}
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
}
}
},
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
@Composable
private fun StatusCard(
kernelVersion: KernelVersion,
ksuVersion: Int?,
lkmMode: Boolean?,
onClickInstall: () -> Unit = {}
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
if (kernelVersion.isGKI()) {
onClickInstall()
}
}
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
when {
ksuVersion != null -> {
val safeMode = when {
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
else -> ""
}
val workingMode = when (lkmMode) {
null -> " <Non-GKI>"
true -> " <LKM>"
else -> " <GKI>"
}
val workingText =
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_version", false)
val isHideOtherInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_other_info", false)
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false)
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
Column(Modifier.padding(start = 20.dp)) {
Text(
text = workingText,
style = MaterialTheme.typography.titleMedium
)
if (!isHideVersion) {
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_working_version, ksuVersion),
style = MaterialTheme.typography.bodyMedium
)
}
if (!isHideOtherInfo) {
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(
R.string.home_superuser_count, getSuperuserCount()
), style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_module_count, getModuleCount()),
style = MaterialTheme.typography.bodyMedium
)
val kpmVersion = getKpmVersion()
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
style = MaterialTheme.typography.bodyMedium
)
}
}
if (!isHideSusfsStatus) {
Spacer(modifier = Modifier.height(4.dp))
val suSFS = getSuSFS()
if (lkmMode != true) {
val translatedStatus = when (suSFS) {
"Supported" -> stringResource(R.string.status_supported)
"Not Supported" -> stringResource(R.string.status_not_supported)
else -> stringResource(R.string.status_unknown)
}
Text(
text = stringResource(R.string.home_susfs, translatedStatus),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
kernelVersion.isGKI() -> {
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
Column(Modifier.padding(start = 20.dp)) {
Text(
text = stringResource(R.string.home_not_installed),
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_install),
style = MaterialTheme.typography.bodyMedium
)
}
}
else -> {
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
Column(Modifier.padding(start = 20.dp)) {
Text(
text = stringResource(R.string.home_unsupported),
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_unsupported_reason),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}
@Composable
fun WarningCard(
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.padding(24.dp)
) {
Text(
text = message, style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun ContributionCard() {
val uriHandler = LocalUriHandler.current
val links = listOf("https://github.com/zako", "https://github.com/udochina")
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
val randomIndex = Random.nextInt(links.size)
uriHandler.openUri(links[randomIndex])
}
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(R.string.home_ContributionCard_kernelsu),
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
@Composable
fun LearnMoreCard() {
val uriHandler = LocalUriHandler.current
val url = stringResource(R.string.home_learn_kernelsu_url)
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri(url)
}
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
Column {
Text(
text = stringResource(R.string.home_learn_kernelsu),
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_learn_kernelsu),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
@Composable
fun DonateCard() {
val uriHandler = LocalUriHandler.current
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri("https://patreon.com/weishu")
}
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
Column {
Text(
text = stringResource(R.string.home_support_title),
style = MaterialTheme.typography.titleSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_support_content),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
@Composable
private fun InfoCard() {
val context = LocalContext.current
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_simple_mode", false)
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
) withContext@{
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)
}
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
if (!isSimpleMode) {
Spacer(Modifier.height(16.dp))
val androidVersion = Build.VERSION.RELEASE
InfoCardItem(stringResource(R.string.home_android_version), androidVersion)
}
Spacer(Modifier.height(16.dp))
val deviceModel = getDeviceModel(context)
InfoCardItem(stringResource(R.string.home_device_model), deviceModel)
Spacer(Modifier.height(16.dp))
val managerVersion = getManagerVersion(context)
InfoCardItem(
stringResource(R.string.home_manager_version),
"${managerVersion.first} (${managerVersion.second})"
)
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
if (!isSimpleMode) {
val kpmVersion = getKpmVersion()
var displayVersion: String
val isKpmConfigured = checkKpmConfigured()
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
val statusText = if (isKpmConfigured) {
stringResource(R.string.kernel_patched)
} else {
stringResource(R.string.kernel_not_enabled)
}
displayVersion = "${stringResource(R.string.not_supported)} ($statusText)"
} else {
displayVersion = "${stringResource(R.string.supported)} ($kpmVersion)"
}
Spacer(Modifier.height(16.dp))
InfoCardItem(stringResource(R.string.home_kpm_version), displayVersion)
}
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_susfs_status", false)
if ((!isSimpleMode) && (!isHideSusfsStatus)) {
Spacer(modifier = Modifier.height(16.dp))
val suSFS = getSuSFS()
if (suSFS == "Supported") {
val suSFSVersion = getSuSFSVersion()
if (suSFSVersion.isEmpty()) return@withContext
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
val infoText = buildString {
append(suSFSVersion)
append(if (isSUS_SU) " (${getSuSFSVariant()})" else " (${stringResource(R.string.manual_hook)})")
if (isSUS_SU) {
val susSUMode = try { susfsSUS_SU_Mode().toString() } catch (_: Exception) { "" }
if (susSUMode.isNotEmpty()) {
append(" ${stringResource(R.string.sus_su_mode)} $susSUMode")
}
}
}
InfoCardItem(
stringResource(R.string.home_susfs_version),
infoText
)
}
}
}
}
}
fun getManagerVersion(context: Context): Pair<String, Long> {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
return Pair(packageInfo.versionName!!, versionCode)
}
@Preview
@Composable
private fun StatusCardPreview() {
Column {
StatusCard(KernelVersion(5, 10, 101), 1, null)
StatusCard(KernelVersion(5, 10, 101), 20000, true)
StatusCard(KernelVersion(5, 10, 101), null, true)
StatusCard(KernelVersion(4, 10, 101), null, false)
}
}
@Preview
@Composable
private fun WarningCardPreview() {
Column {
WarningCard(message = "Warning message")
WarningCard(
message = "Warning message ",
MaterialTheme.colorScheme.outlineVariant,
onClick = {})
}
}
@SuppressLint("PrivateApi")
private fun getDeviceModel(context: Context): String {
return try {
val systemProperties = Class.forName("android.os.SystemProperties")
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
val marketNameKeys = listOf(
"ro.product.marketname", // Xiaomi
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
"ro.vivo.market.name", // Vivo
"ro.config.marketing_name" // Huawei
)
for (key in marketNameKeys) {
val marketName = getMethod.invoke(null, key, "") as String
if (marketName.isNotEmpty()) {
return marketName
}
}
Build.DEVICE
} catch (e: Exception) {
Build.DEVICE
}
}
private fun checkKpmConfigured(): Boolean {
try {
val process = Runtime.getRuntime().exec("su -c cat /proc/config.gz")
val inputStream = process.inputStream
val gzipInputStream = GZIPInputStream(inputStream)
val reader = BufferedReader(InputStreamReader(gzipInputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line?.contains("CONFIG_KPM=y") == true) {
return true
}
}
reader.close()
} catch (e: Exception) {
e.printStackTrace()
}
return false
}

View File

@@ -1,606 +0,0 @@
package zako.zako.zako.ui.screen
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
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.FileUpload
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import com.maxkeppeler.sheets.list.models.ListOption
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import zako.zako.zako.ui.component.DialogHandle
import zako.zako.zako.ui.component.rememberConfirmDialog
import zako.zako.zako.ui.component.rememberCustomDialog
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.ui.theme.getCardColors
import zako.zako.zako.ui.theme.getCardElevation
import zako.zako.zako.ui.util.*
import zako.zako.zako.R
import zako.zako.zako.utils.AssetsUtil
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
/**
* @author weishu
* @date 2024/3/12.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun InstallScreen(navigator: DestinationsNavigator) {
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) }
val onFlashComplete = {
showRebootDialog = true
}
if (showRebootDialog) {
RebootDialog(
show = true,
onDismiss = { showRebootDialog = false },
onConfirm = {
showRebootDialog = false
try {
val process = Runtime.getRuntime().exec("su")
process.outputStream.bufferedWriter().use { writer ->
writer.write("svc power reboot\n")
writer.write("exit\n")
}
} catch (e: Exception) {
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
}
}
)
}
val onInstall = {
installMethod?.let { method ->
when (method) {
is InstallMethod.HorizonKernel -> {
method.uri?.let { uri ->
val worker = HorizonKernelWorker(context)
worker.uri = uri
worker.setOnFlashCompleteListener(onFlashComplete)
worker.start()
}
}
else -> {
val flashIt = FlashIt.FlashBoot(
boot = if (method is InstallMethod.SelectFile) method.uri else null,
lkm = lkmSelection,
ota = method is InstallMethod.DirectInstallToInactiveSlot
)
navigator.navigate(FlashScreenDestination(flashIt))
}
}
}
Unit
}
val currentKmi by produceState(initialValue = "") {
value = getCurrentKmi()
}
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
kmi?.let {
lkmSelection = LkmSelection.KmiString(it)
onInstall()
}
}
val onClickNext = {
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
selectKmiDialog.show()
} else {
onInstall()
}
}
val selectLkmLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
lkmSelection = LkmSelection.LkmUri(uri)
}
}
}
val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
})
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
TopBar(
onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
) {
SelectInstallMethod { method ->
installMethod = method
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
(lkmSelection as? LkmSelection.LkmUri)?.let {
Text(
stringResource(
id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)"
)
)
}
Button(
modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null,
onClick = onClickNext
) {
Text(
stringResource(id = R.string.install_next),
fontSize = MaterialTheme.typography.bodyMedium.fontSize
)
}
}
}
}
}
@Composable
private fun RebootDialog(
show: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
if (show) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(id = R.string.yes))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(id = R.string.no))
}
}
)
}
}
private class HorizonKernelWorker(private val context: Context) : Thread() {
var uri: Uri? = null
private lateinit var filePath: String
private lateinit var binaryPath: String
private var onFlashComplete: (() -> Unit)? = null
fun setOnFlashCompleteListener(listener: () -> Unit) {
onFlashComplete = listener
}
override fun run() {
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
try {
cleanup()
if (!rootAvailable()) {
showError(context.getString(R.string.root_required))
return
}
copy()
if (!File(filePath).exists()) {
showError(context.getString(R.string.copy_failed))
return
}
getBinary()
patch()
flash()
(context as? Activity)?.runOnUiThread {
onFlashComplete?.invoke()
}
} catch (e: Exception) {
showError(e.message ?: context.getString(R.string.unknown_error))
}
}
private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
}
private fun copy() {
uri?.let { safeUri ->
context.contentResolver.openInputStream(safeUri)?.use { input ->
FileOutputStream(File(filePath)).use { output ->
input.copyTo(output)
}
}
}
}
private fun getBinary() {
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
if (!File(binaryPath).exists()) {
throw IOException("Failed to extract update-binary")
}
}
private fun patch() {
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
}
private fun flash() {
val process = ProcessBuilder("su")
.redirectErrorStream(true)
.start()
try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
writer.flush()
}
process.inputStream.bufferedReader().use { reader ->
reader.lineSequence().forEach { line ->
if (line.startsWith("ui_print")) {
showLog(line.removePrefix("ui_print"))
}
}
}
} finally {
process.destroy()
}
if (!File("${context.filesDir.absolutePath}/done").exists()) {
throw IOException("Flash failed")
}
}
private fun runCommand(su: Boolean, cmd: String): Int {
val process = ProcessBuilder(if (su) "su" else "sh")
.redirectErrorStream(true)
.start()
return try {
process.outputStream.bufferedWriter().use { writer ->
writer.write("$cmd\n")
writer.write("exit\n")
writer.flush()
}
process.waitFor()
} finally {
process.destroy()
}
}
private fun showError(message: String) {
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
private fun showLog(message: String) {
(context as? Activity)?.runOnUiThread {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
}
sealed class InstallMethod {
data class SelectFile(
val uri: Uri? = null,
@StringRes override val label: Int = R.string.select_file,
override val summary: String?
) : InstallMethod()
data object DirectInstall : InstallMethod() {
override val label: Int
get() = R.string.direct_install
}
data object DirectInstallToInactiveSlot : InstallMethod() {
override val label: Int
get() = R.string.install_inactive_slot
}
data class HorizonKernel(
val uri: Uri? = null,
@StringRes override val label: Int = R.string.horizon_kernel,
override val summary: String? = null
) : InstallMethod()
abstract val label: Int
open val summary: String? = null
}
@Composable
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
val selectFileTip = stringResource(
id = R.string.select_file_tip,
if (isInitBoot()) "init_boot" else "boot"
)
val radioOptions = mutableListOf<InstallMethod>(
InstallMethod.SelectFile(summary = selectFileTip)
)
if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall)
if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
}
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
}
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
val option = when (currentSelectingMethod) {
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
else -> null
}
option?.let {
selectedOption = it
onSelected(it)
}
}
}
}
val confirmDialog = rememberConfirmDialog(
onConfirm = {
selectedOption = InstallMethod.DirectInstallToInactiveSlot
onSelected(InstallMethod.DirectInstallToInactiveSlot)
},
onDismiss = null
)
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
val onClick = { option: InstallMethod ->
currentSelectingMethod = option
when (option) {
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
})
}
is InstallMethod.DirectInstall -> {
selectedOption = option
onSelected(option)
}
is InstallMethod.DirectInstallToInactiveSlot -> {
confirmDialog.showConfirm(dialogTitle, dialogContent)
}
}
}
Column {
radioOptions.forEach { option ->
val interactionSource = remember { MutableInteractionSource() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.toggleable(
value = option.javaClass == selectedOption?.javaClass,
onValueChange = { onClick(option) },
role = Role.RadioButton,
indication = LocalIndication.current,
interactionSource = interactionSource
)
) {
RadioButton(
selected = option.javaClass == selectedOption?.javaClass,
onClick = { onClick(option) },
interactionSource = interactionSource
)
Column(
modifier = Modifier.padding(vertical = 12.dp)
) {
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
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val supportedKmi by produceState(initialValue = emptyList<String>()) {
value = getSupportedKmis()
}
val listOptions = supportedKmi.map { value ->
ListOption(
titleText = value,
subtitleText = null,
icon = null
)
}
var selection: String? = null
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
AlertDialog(
onDismissRequest = {
dismiss()
},
title = {
Text(text = stringResource(R.string.select_kmi))
},
text = {
Column {
listOptions.forEachIndexed { index, option ->
Row(
modifier = Modifier
.clickable {
selection = supportedKmi[index]
}
.padding(vertical = 8.dp)
) {
Column {
Text(text = option.titleText)
option.subtitleText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
},
confirmButton = {
TextButton(
onClick = {
if (selection != null) {
onSelected(selection)
}
dismiss()
}
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
}
) {
Text(text = stringResource(android.R.string.cancel))
}
},
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.medium,
tonalElevation = getCardElevation()
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
onBack: () -> Unit = {},
onLkmUpload: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TopAppBar(
title = { Text(stringResource(R.string.install)) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
actions = {
IconButton(onClick = onLkmUpload) {
Icon(Icons.Filled.FileUpload, contentDescription = null)
}
},
windowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
scrollBehavior = scrollBehavior
)
}
@Preview
@Composable
fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator)
}

View File

@@ -1,615 +0,0 @@
package zako.zako.zako.ui.screen
import androidx.compose.animation.AnimatedVisibility
import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.compose.foundation.layout.*
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.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.unit.DpSize
import androidx.compose.ui.unit.dp
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.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import zako.zako.zako.ui.component.SwitchItem
import zako.zako.zako.ui.theme.CardConfig
import zako.zako.zako.ui.theme.ThemeColors
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.ui.theme.saveCustomBackground
import zako.zako.zako.ui.theme.saveThemeColors
import zako.zako.zako.ui.theme.saveThemeMode
import zako.zako.zako.ui.theme.saveDynamicColorState
import zako.zako.zako.ui.util.getSuSFS
import zako.zako.zako.ui.util.getSuSFSFeatures
import zako.zako.zako.ui.util.susfsSUS_SU_0
import zako.zako.zako.ui.util.susfsSUS_SU_2
import zako.zako.zako.ui.util.susfsSUS_SU_Mode
import androidx.core.content.edit
import zako.zako.zako.R
fun saveCardConfig(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
with(prefs.edit()) {
putFloat("card_alpha", CardConfig.cardAlpha)
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
apply()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
// 主题模式选择
var themeMode by remember {
mutableIntStateOf(
when(ThemeConfig.forceDarkMode) {
true -> 2 // 深色
false -> 1 // 浅色
null -> 0 // 跟随系统
}
)
}
// 动态颜色开关状态
var useDynamicColor by remember {
mutableStateOf(ThemeConfig.useDynamicColor)
}
var showThemeModeDialog by remember { mutableStateOf(false) }
// 主题模式选项
val themeOptions = listOf(
stringResource(R.string.theme_follow_system),
stringResource(R.string.theme_light),
stringResource(R.string.theme_dark)
)
// 简洁模块开关状态
var isSimpleMode by remember {
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
}
// 更新简洁模块开关状态
val onSimpleModeChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_simple_mode", newValue) }
isSimpleMode = newValue
}
// 隐藏内核 KernelSU 版本号开关状态
var isHideVersion by remember {
mutableStateOf(prefs.getBoolean("is_hide_version", false))
}
// 隐藏内核 KernelSU 版本号模块开关状态
val onHideVersionChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_version", newValue) }
isHideVersion = newValue
}
// 隐藏模块数量等信息开关状态
var isHideOtherInfo by remember {
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
}
// 隐藏模块数量等信息开关状态
val onHideOtherInfoChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_other_info", newValue) }
isHideOtherInfo = newValue
}
// 隐藏 SuSFS 状态开关状态
var isHideSusfsStatus by remember {
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
}
// 隐藏 SuSFS 状态开关状态
val onHideSusfsStatusChange = { newValue: Boolean ->
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
isHideSusfsStatus = newValue
}
// SELinux 状态
var selinuxEnabled by remember {
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
}
// 卡片配置状态
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
var showCardSettings by remember { mutableStateOf(false) }
var isCustomBackgroundEnabled by rememberSaveable {
mutableStateOf(ThemeConfig.customBackgroundUri != null)
}
// 初始化卡片配置
val systemIsDark = isSystemInDarkTheme()
LaunchedEffect(Unit) {
CardConfig.apply {
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
// 如果没有手动设置透明度,且是深色模式,则使用默认值
if (!isCustomAlphaSet) {
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
if (isDarkMode) {
cardAlpha = 0.35f
}
}
}
themeMode = when (ThemeConfig.forceDarkMode) {
true -> 2
false -> 1
null -> 0
}
}
// 主题色选项
val themeColorOptions = listOf(
stringResource(R.string.color_default) to ThemeColors.Default,
stringResource(R.string.color_blue) to ThemeColors.Blue,
stringResource(R.string.color_green) to ThemeColors.Green,
stringResource(R.string.color_purple) to ThemeColors.Purple,
stringResource(R.string.color_orange) to ThemeColors.Orange,
stringResource(R.string.color_pink) to ThemeColors.Pink,
stringResource(R.string.color_gray) to ThemeColors.Gray,
stringResource(R.string.color_yellow) to ThemeColors.Yellow
)
var showThemeColorDialog by remember { mutableStateOf(false) }
// 图片选择器
val pickImageLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
context.saveCustomBackground(it)
isCustomBackgroundEnabled = true
CardConfig.cardElevation = 0.dp
saveCardConfig(context)
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.more_settings)) },
navigationIcon = {
IconButton(onClick = { navigator.popBackStack() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
}
},
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(top = 12.dp)
) {
// SELinux 开关
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.selinux),
summary = if (selinuxEnabled)
stringResource(R.string.selinux_enabled) else
stringResource(R.string.selinux_disabled),
checked = selinuxEnabled
) { enabled ->
val command = if (enabled) "setenforce 1" else "setenforce 0"
Shell.getShell().newJob().add(command).exec().let { result ->
if (result.isSuccess) selinuxEnabled = enabled
}
}
var isExpanded by remember { mutableStateOf(false) }
ListItem(
leadingContent = { Icon(Icons.Filled.AutoFixHigh, null) },
headlineContent = { Text(stringResource(R.string.custom_settings)) },
modifier = Modifier.clickable {
isExpanded = !isExpanded
}
)
AnimatedVisibility(
visible = isExpanded,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
// 添加简洁模块开关
SwitchItem(
icon = Icons.Filled.Brush,
title = stringResource(R.string.simple_mode),
summary = stringResource(R.string.simple_mode_summary),
checked = isSimpleMode
) {
onSimpleModeChange(it)
}
}
AnimatedVisibility(
visible = isExpanded,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
// 隐藏内核部分版本号
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_kernel_kernelsu_version),
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
checked = isHideVersion
) {
onHideVersionChange(it)
}
}
AnimatedVisibility(
visible = isExpanded,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
// 模块数量等信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_other_info),
summary = stringResource(R.string.hide_other_info_summary),
checked = isHideOtherInfo
) {
onHideOtherInfoChange(it)
}
}
AnimatedVisibility(
visible = isExpanded,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
// SuSFS 状态信息
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_susfs_status),
summary = stringResource(R.string.hide_susfs_status_summary),
checked = isHideSusfsStatus
) {
onHideSusfsStatusChange(it)
}
}
// region SUSFS 配置(仅在支持时显示)
val suSFS = getSuSFS()
val isSUS_SU = getSuSFSFeatures()
if (suSFS == "Supported") {
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
// 初始化时,默认启用
var isEnabled by rememberSaveable {
mutableStateOf(true) // 默认启用
}
// 在启动时检查状态
LaunchedEffect(Unit) {
// 如果当前模式不是2就强制启用
val currentMode = susfsSUS_SU_Mode()
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
if (currentMode != "2" && wasManuallyDisabled) {
susfsSUS_SU_2() // 强制切换到模式2
prefs.edit { putBoolean("enable_sus_su", true) }
}
isEnabled = currentMode == "2"
}
SwitchItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(id = R.string.settings_susfs_toggle),
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
checked = isEnabled
) {
if (it) {
// 手动启用
susfsSUS_SU_2()
prefs.edit { putBoolean("enable_sus_su", true) }
} else {
// 手动关闭
susfsSUS_SU_0()
prefs.edit { putBoolean("enable_sus_su", false) }
}
isEnabled = it
}
}
}
// endregion
// 动态颜色开关
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SwitchItem(
icon = Icons.Filled.ColorLens,
title = stringResource(R.string.dynamic_color_title),
summary = stringResource(R.string.dynamic_color_summary),
checked = useDynamicColor
) { enabled ->
useDynamicColor = enabled
context.saveDynamicColorState(enabled)
}
}
// 只在未启用动态颜色时显示主题色选择
AnimatedVisibility(
visible = !useDynamicColor
) {
ListItem(
leadingContent = { Icon(Icons.Default.Palette, null) },
headlineContent = { Text(stringResource(R.string.theme_color)) },
supportingContent = {
val currentThemeName = when (ThemeConfig.currentTheme) {
is ThemeColors.Default -> stringResource(R.string.color_default)
is ThemeColors.Blue -> stringResource(R.string.color_blue)
is ThemeColors.Green -> stringResource(R.string.color_green)
is ThemeColors.Purple -> stringResource(R.string.color_purple)
is ThemeColors.Orange -> stringResource(R.string.color_orange)
is ThemeColors.Pink -> stringResource(R.string.color_pink)
is ThemeColors.Gray -> stringResource(R.string.color_gray)
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
else -> stringResource(R.string.color_default)
}
Text(currentThemeName)
},
modifier = Modifier.clickable { showThemeColorDialog = true }
)
if (showThemeColorDialog) {
AlertDialog(
onDismissRequest = { showThemeColorDialog = false },
title = { Text(stringResource(R.string.choose_theme_color)) },
text = {
Column {
themeColorOptions.forEach { (name, theme) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
context.saveThemeColors(when (theme) {
ThemeColors.Default -> "default"
ThemeColors.Blue -> "blue"
ThemeColors.Green -> "green"
ThemeColors.Purple -> "purple"
ThemeColors.Orange -> "orange"
ThemeColors.Pink -> "pink"
ThemeColors.Gray -> "gray"
ThemeColors.Yellow -> "yellow"
else -> "default"
})
showThemeColorDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = ThemeConfig.currentTheme::class == theme::class,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.size(24.dp)
.background(theme.Primary, shape = CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
Text(name)
}
}
}
},
confirmButton = {}
)
}
}
// 自定义背景开关
ListItem(
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
modifier = Modifier.clickable {
if (isCustomBackgroundEnabled) {
showCardSettings = !showCardSettings
}
},
trailingContent = {
Switch(
checked = isCustomBackgroundEnabled,
onCheckedChange = { isChecked ->
if (isChecked) {
pickImageLauncher.launch("image/*")
} else {
context.saveCustomBackground(null)
isCustomBackgroundEnabled = false
CardConfig.cardElevation = CardConfig.defaultElevation
CardConfig.cardAlpha = 0.45f
CardConfig.isCustomAlphaSet = false
saveCardConfig(context)
cardAlpha = 0.35f
themeMode = 0
context.saveThemeMode(null)
CardConfig.isUserDarkModeEnabled = false
CardConfig.isUserLightModeEnabled = false
CardConfig.save(context)
}
}
)
}
)
// 透明度 Slider
AnimatedVisibility(
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
ListItem(
leadingContent = { Icon(Icons.Filled.Opacity, null) },
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
supportingContent = {
Slider(
value = cardAlpha,
onValueChange = { newValue ->
cardAlpha = newValue
CardConfig.cardAlpha = newValue
CardConfig.isCustomAlphaSet = true
prefs.edit { putBoolean("is_custom_alpha_set", true) }
prefs.edit { putFloat("card_alpha", newValue) }
},
onValueChangeFinished = {
CoroutineScope(Dispatchers.IO).launch {
saveCardConfig(context)
}
},
valueRange = 0f..1f,
colors = getSliderColors(cardAlpha, useCustomColors = true),
thumb = {
SliderDefaults.Thumb(
interactionSource = remember { MutableInteractionSource() },
thumbSize = DpSize(0.dp, 0.dp)
)
}
)
}
)
}
AnimatedVisibility(
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
){
ListItem(
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
headlineContent = { Text(stringResource(R.string.theme_mode)) },
supportingContent = { Text(themeOptions[themeMode]) },
modifier = Modifier.clickable {
showThemeModeDialog = true
}
)
}
// 主题模式选择对话框
if (showThemeModeDialog) {
AlertDialog(
onDismissRequest = { showThemeModeDialog = false },
title = { Text(stringResource(R.string.theme_mode)) },
text = {
Column {
themeOptions.forEachIndexed { index, option ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
themeMode = index
val newThemeMode = when(index) {
0 -> null // 跟随系统
1 -> false // 浅色
2 -> true // 深色
else -> null
}
context.saveThemeMode(newThemeMode)
when (index) {
2 -> {
ThemeConfig.forceDarkMode = true
CardConfig.isUserLightModeEnabled = false
CardConfig.isUserDarkModeEnabled = true
CardConfig.save(context)
}
1 -> {
ThemeConfig.forceDarkMode = false
CardConfig.isUserLightModeEnabled = true
CardConfig.isUserDarkModeEnabled = false
CardConfig.save(context)
}
0 -> {
ThemeConfig.forceDarkMode = null
CardConfig.isUserLightModeEnabled = false
CardConfig.isUserDarkModeEnabled = false
CardConfig.save(context)
}
}
showThemeModeDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = themeMode == index,
onClick = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(option)
}
}
}
},
confirmButton = {}
)
}
}
}
}
@Composable
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
val theme = ThemeConfig.currentTheme
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
val useDynamicColor = ThemeConfig.useDynamicColor
return when {
// 使用动态颜色时
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
thumbColor = MaterialTheme.colorScheme.primary
)
}
// 使用自定义主题色时
useCustomColors -> {
SliderDefaults.colors(
activeTrackColor = theme.getCustomSliderActiveColor(),
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
thumbColor = theme.Primary
)
}
else -> {
val activeColor = if (isDarkTheme) {
theme.Primary.copy(alpha = cardAlpha)
} else {
theme.Primary.copy(alpha = cardAlpha)
}
val inactiveColor = if (isDarkTheme) {
Color.DarkGray.copy(alpha = 0.3f)
} else {
Color.LightGray.copy(alpha = 0.3f)
}
SliderDefaults.colors(
activeTrackColor = activeColor,
inactiveTrackColor = inactiveColor,
thumbColor = activeColor
)
}
}
}

View File

@@ -1,547 +0,0 @@
package zako.zako.zako.ui.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.AlertDialog
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.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.core.content.edit
import com.maxkeppeker.sheets.core.models.base.IconSource
import com.maxkeppeler.sheets.list.models.ListOption
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import zako.zako.zako.BuildConfig
import zako.zako.zako.Natives
import zako.zako.zako.R
import zako.zako.zako.ui.component.AboutDialog
import zako.zako.zako.ui.component.ConfirmResult
import zako.zako.zako.ui.component.DialogHandle
import zako.zako.zako.ui.component.SwitchItem
import zako.zako.zako.ui.component.rememberConfirmDialog
import zako.zako.zako.ui.component.rememberCustomDialog
import zako.zako.zako.ui.component.rememberLoadingDialog
import zako.zako.zako.ui.theme.CardConfig
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.ui.theme.getCardColors
import zako.zako.zako.ui.theme.getCardElevation
import zako.zako.zako.ui.util.LocalSnackbarHost
import zako.zako.zako.ui.util.getBugreportFile
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* @author weishu
* @date 2023/1/1.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SettingScreen(navigator: DestinationsNavigator) {
// region 界面基础设置
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
// endregion
Scaffold(
topBar = {
TopBar(
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
val aboutDialog = rememberCustomDialog {
AboutDialog(it)
}
val loadingDialog = rememberLoadingDialog()
// endregion
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
) {
// region 上下文与协程
val context = LocalContext.current
val scope = rememberCoroutineScope()
// endregion
// region 日志导出功能
val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
loadingDialog.show()
context.contentResolver.openOutputStream(uri)?.use { output ->
getBugreportFile(context).inputStream().use {
it.copyTo(output)
}
}
loadingDialog.hide()
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
}
}
// region 配置项列表
// 配置文件模板入口
val profileTemplate = stringResource(id = R.string.settings_profile_template)
ListItem(
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
headlineContent = { Text(profileTemplate) },
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
modifier = Modifier.clickable {
navigator.navigate(AppProfileTemplateScreenDestination)
}
)
// 卸载模块开关
var umountChecked by rememberSaveable {
mutableStateOf(Natives.isDefaultUmountModules())
}
SwitchItem(
icon = Icons.Filled.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
checked = umountChecked
) {
if (Natives.setDefaultUmountModules(it)) {
umountChecked = it
}
}
// SU 禁用开关(仅在兼容版本显示)
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled())
}
SwitchItem(
icon = Icons.Filled.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary),
checked = isSuDisabled,
) { checked ->
val shouldEnable = !checked
if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = !shouldEnable
}
}
}
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
// 更新检查开关
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
}
// Web调试开关
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
) {
prefs.edit { putBoolean("enable_web_debugging", it) }
enableWebDebugging = it
}
// 更多设置
val newButtonTitle = stringResource(id = R.string.more_settings)
ListItem(
leadingContent = {
Icon(
Icons.Filled.Settings,
contentDescription = newButtonTitle
)
},
headlineContent = { Text(newButtonTitle) },
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
modifier = Modifier.clickable {
navigator.navigate(MoreSettingsScreenDestination)
}
)
var showBottomsheet by remember { mutableStateOf(false) }
ListItem(
leadingContent = {
Icon(
Icons.Filled.BugReport,
stringResource(id = R.string.send_log)
)
},
headlineContent = { Text(stringResource(id = R.string.send_log)) },
modifier = Modifier.clickable {
showBottomsheet = true
}
)
if (showBottomsheet) {
ModalBottomSheet(
onDismissRequest = { showBottomsheet = false },
content = {
Row(
modifier = Modifier
.padding(10.dp)
.align(Alignment.CenterHorizontally)
) {
Box {
Column(
modifier = Modifier
.padding(16.dp)
.clickable {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
showBottomsheet = 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)
)
)
}
}
) {
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
)
}
)
}
}
}
}
)
}
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) {
UninstallItem(navigator) {
loadingDialog.withLoading(it)
}
}
val about = stringResource(id = R.string.about)
ListItem(
leadingContent = {
Icon(
Icons.Filled.ContactPage,
about
)
},
headlineContent = { Text(about) },
modifier = Modifier.clickable {
aboutDialog.show()
}
)
}
}
}
@Composable
fun UninstallItem(
navigator: DestinationsNavigator,
withLoading: suspend (suspend () -> Unit) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val uninstallConfirmDialog = rememberConfirmDialog()
val showTodo = {
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
}
val uninstallDialog = rememberUninstallDialog { uninstallType ->
scope.launch {
val result = uninstallConfirmDialog.awaitConfirm(
title = context.getString(uninstallType.title),
content = context.getString(uninstallType.message)
)
if (result == ConfirmResult.Confirmed) {
withLoading {
when (uninstallType) {
UninstallType.TEMPORARY -> showTodo()
UninstallType.PERMANENT -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashUninstall)
)
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashRestore)
)
UninstallType.NONE -> Unit
}
}
}
}
}
val uninstall = stringResource(id = R.string.settings_uninstall)
ListItem(
leadingContent = {
Icon(
Icons.Filled.Delete,
uninstall
)
},
headlineContent = { Text(uninstall) },
modifier = Modifier.clickable {
uninstallDialog.show()
}
)
}
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
TEMPORARY(
R.string.settings_uninstall_temporary,
R.string.settings_uninstall_temporary_message,
Icons.Filled.Delete
),
PERMANENT(
R.string.settings_uninstall_permanent,
R.string.settings_uninstall_permanent_message,
Icons.Filled.DeleteForever
),
RESTORE_STOCK_IMAGE(
R.string.settings_restore_stock_image,
R.string.settings_restore_stock_image_message,
Icons.AutoMirrored.Filled.Undo
),
NONE(0, 0, Icons.Filled.Delete)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val options = listOf(
// UninstallType.TEMPORARY,
UninstallType.PERMANENT,
UninstallType.RESTORE_STOCK_IMAGE
)
val listOptions = options.map {
ListOption(
titleText = stringResource(it.title),
subtitleText = if (it.message != 0) stringResource(it.message) else null,
icon = IconSource(it.icon)
)
}
var selection = UninstallType.NONE
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
AlertDialog(
onDismissRequest = {
dismiss()
},
title = {
Text(text = stringResource(R.string.settings_uninstall))
},
text = {
Column {
listOptions.forEachIndexed { index, option ->
Row(
modifier = Modifier
.clickable {
selection = options[index]
}
.padding(vertical = 8.dp)
) {
Icon(
imageVector = options[index].icon,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Column {
Text(text = option.titleText)
option.subtitleText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
},
confirmButton = {
androidx.compose.material3.TextButton(
onClick = {
if (selection != UninstallType.NONE) {
onSelected(selection)
}
dismiss()
}
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
androidx.compose.material3.TextButton(
onClick = {
dismiss()
}
) {
Text(text = stringResource(android.R.string.cancel))
}
},
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
shape = MaterialTheme.shapes.medium,
tonalElevation = getCardElevation()
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val cardColor = MaterialTheme.colorScheme.secondaryContainer
val cardAlpha = CardConfig.cardAlpha
TopAppBar(
title = { Text(stringResource(R.string.settings)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
@Preview
@Composable
private fun SettingsPreview() {
SettingScreen(EmptyDestinationsNavigator)
}

View File

@@ -1,393 +0,0 @@
package zako.zako.zako.ui.screen
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
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.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import zako.zako.zako.R
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.launch
import zako.zako.zako.Natives
import zako.zako.zako.ui.component.SearchAppBar
import zako.zako.zako.ui.util.ModuleModify
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SuperUserScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<SuperUserViewModel>()
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val listState = rememberLazyListState()
val context = LocalContext.current
val snackBarHostState = remember { SnackbarHostState() }
// 添加备份和还原启动器
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
LaunchedEffect(key1 = navigator) {
viewModel.search = ""
if (viewModel.appList.isEmpty()) {
viewModel.fetchAppList()
}
}
LaunchedEffect(viewModel.search) {
if (viewModel.search.isEmpty()) {
listState.scrollToItem(0)
}
}
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.superuser)) },
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)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
DropdownMenuItem(text = {
Text(stringResource(R.string.refresh))
}, onClick = {
scope.launch {
viewModel.fetchAppList()
}
showDropdown = false
})
DropdownMenuItem(text = {
Text(
if (viewModel.showSystemApps) {
stringResource(R.string.hide_system_apps)
} else {
stringResource(R.string.show_system_apps)
}
)
}, onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false
})
DropdownMenuItem(text = {
Text(stringResource(R.string.backup_allowlist))
}, onClick = {
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
showDropdown = false
})
DropdownMenuItem(text = {
Text(stringResource(R.string.restore_allowlist))
}, onClick = {
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
showDropdown = false
})
}
}
},
scrollBehavior = scrollBehavior
)
},
snackbarHost = { SnackbarHost(snackBarHostState) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
bottomBar = {
// 批量操作按钮,直接放在底部栏
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
onClick = {
scope.launch {
viewModel.updateBatchPermissions(true)
}
}
) {
Text(stringResource(R.string.batch_authorization))
}
Button(
onClick = {
scope.launch {
viewModel.updateBatchPermissions(false)
}
}
) {
Text(stringResource(R.string.batch_cancel_authorization))
}
}
}
}
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier.padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
val rootApps = viewModel.appList.filter { it.allowSu }
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
// 显示ROOT权限应用组
if (rootApps.isNotEmpty()) {
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
// 显示自定义配置应用组
if (customApps.isNotEmpty()) {
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
// 显示其他应用组
if (otherApps.isNotEmpty()) {
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
AppItem(
app = app,
isSelected = viewModel.selectedApps.contains(app.packageName),
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
onSwitchChange = { allowSu ->
scope.launch {
val profile = Natives.getAppProfile(app.packageName, app.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
viewModel.fetchAppList()
}
}
},
onClick = {
if (viewModel.showBatchActions) {
viewModel.toggleAppSelection(app.packageName)
} else {
navigator.navigate(AppProfileScreenDestination(app))
}
},
onLongClick = {
// 长按进入多选模式
if (!viewModel.showBatchActions) {
viewModel.toggleBatchMode()
viewModel.toggleAppSelection(app.packageName)
}
},
viewModel = viewModel
)
}
}
}
}
}
}
@Composable
fun GroupHeader(title: String) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = title,
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun AppItem(
app: SuperUserViewModel.AppInfo,
isSelected: Boolean,
onToggleSelection: () -> Unit,
onSwitchChange: (Boolean) -> Unit,
onClick: () -> Unit,
onLongClick: () -> Unit,
viewModel: SuperUserViewModel
) {
ListItem(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { onLongClick() },
onTap = { onClick() }
)
},
headlineContent = { Text(app.label) },
supportingContent = {
Column {
Text(app.packageName)
FlowRow {
if (app.allowSu) {
LabelText(label = "ROOT")
} else {
if (Natives.uidShouldUmount(app.uid)) {
LabelText(label = "UMOUNT")
}
}
if (app.hasCustomProfile) {
LabelText(label = "CUSTOM")
}
}
}
},
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)
)
},
trailingContent = {
if (!viewModel.showBatchActions) {
Switch(
checked = app.allowSu,
onCheckedChange = onSwitchChange
)
} else {
Checkbox(
checked = isSelected,
onCheckedChange = { onToggleSelection() }
)
}
}
)
}
@Composable
fun LabelText(label: String) {
Box(
modifier = Modifier
.padding(top = 4.dp, end = 4.dp)
.background(
Color.Black,
shape = RoundedCornerShape(4.dp)
)
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
style = TextStyle(
fontSize = 8.sp,
color = Color.White,
)
)
}
}

View File

@@ -1,556 +0,0 @@
package zako.zako.zako.ui.screen
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import zako.zako.zako.ui.component.ConfirmResult
import zako.zako.zako.ui.component.SearchAppBar
import zako.zako.zako.ui.component.rememberConfirmDialog
import zako.zako.zako.ui.theme.getCardColors
import zako.zako.zako.ui.theme.getCardElevation
import zako.zako.zako.ui.viewmodel.KpmViewModel
import zako.zako.zako.ui.util.loadKpmModule
import zako.zako.zako.ui.util.unloadKpmModule
import java.io.File
import androidx.core.content.edit
import zako.zako.zako.ui.theme.ThemeConfig
import zako.zako.zako.ui.component.rememberCustomDialog
import zako.zako.zako.ui.component.ConfirmDialogHandle
import zako.zako.zako.R
import java.net.URLDecoder
import java.net.URLEncoder
/**
* KPM 管理界面
* 以下内核模块功能由KernelPatch开发经过修改后加入SukiSU Ultra的内核模块功能
* 开发者zako, Liaokong
*/
var globalModuleFileName: String = ""
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun KpmScreen(
navigator: DestinationsNavigator,
viewModel: KpmViewModel = viewModel()
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog()
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.secondaryContainer
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
val cancel = stringResource(R.string.cancel)
val uninstall = stringResource(R.string.uninstall)
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
val kpmInstallModeDescription = stringResource(R.string.kpm_install_mode_description)
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
val confirmContent = stringResource(R.string.confirm_uninstall_content, globalModuleFileName)
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
val installModeDialog = rememberCustomDialog { dismiss ->
AlertDialog(
onDismissRequest = {
dismiss()
tempFileForInstall?.delete()
tempFileForInstall = null
},
title = { Text(kpmInstallMode) },
text = { Text(kpmInstallModeDescription) },
confirmButton = {
Column {
Button(
onClick = {
scope.launch {
dismiss()
tempFileForInstall?.let { tempFile ->
handleModuleInstall(
tempFile = tempFile,
isEmbed = false,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmInstallSuccess = kpmInstallSuccess,
kpmInstallFailed = kpmInstallFailed
)
}
tempFileForInstall = null
}
}
) {
Text(kpmInstallModeLoad)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
scope.launch {
dismiss()
tempFileForInstall?.let { tempFile ->
handleModuleInstall(
tempFile = tempFile,
isEmbed = true,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmInstallSuccess = kpmInstallSuccess,
kpmInstallFailed = kpmInstallFailed
)
}
tempFileForInstall = null
}
}
) {
Text(kpmInstallModeEmbed)
}
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
tempFileForInstall?.delete()
tempFileForInstall = null
}
) {
Text(cancel)
}
}
)
}
val selectPatchLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
scope.launch {
val fileName = uri.lastPathSegment ?: "unknown.kpm"
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
val tempFile = File(context.cacheDir, encodedFileName)
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
if (!tempFile.name.endsWith(".kpm")) {
snackBarHost.showSnackbar(
message = invalidFileTypeMessage,
duration = SnackbarDuration.Short
)
tempFile.delete()
return@launch
}
tempFileForInstall = tempFile
installModeDialog.show()
}
}
LaunchedEffect(Unit) {
while(true) {
viewModel.fetchModuleList()
delay(5000)
}
}
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.kpm_title)) },
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
scrollBehavior = scrollBehavior,
dropdownContent = {
IconButton(onClick = { viewModel.fetchModuleList() }) {
Icon(
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.refresh)
)
}
}
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
selectPatchLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
}
)
},
icon = {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(R.string.kpm_install)
)
},
text = { Text(stringResource(R.string.kpm_install)) },
containerColor = cardColor.copy(alpha = 1f),
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
},
snackbarHost = { SnackbarHost(snackBarHost) }
) { padding ->
Column(modifier = Modifier.padding(padding)) {
if (!isNoticeClosed) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.kernel_module_notice),
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center
)
IconButton(onClick = {
isNoticeClosed = true
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
}) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = stringResource(R.string.close_notice)
)
}
}
}
if (viewModel.moduleList.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
stringResource(R.string.kpm_empty),
textAlign = TextAlign.Center
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(viewModel.moduleList) { module ->
KpmModuleItem(
module = module,
onUninstall = {
scope.launch {
handleModuleUninstall(
module = module,
viewModel = viewModel,
snackBarHost = snackBarHost,
kpmUninstallSuccess = kpmUninstallSuccess,
kpmUninstallFailed = kpmUninstallFailed,
failedToCheckModuleFile = failedToCheckModuleFile,
uninstall = uninstall,
cancel = cancel,
confirmDialog = confirmDialog,
confirmTitle = confirmTitle,
confirmContent = confirmContent
)
}
},
onControl = {
viewModel.loadModuleDetail(module.id)
}
)
}
}
}
}
}
}
private suspend fun handleModuleInstall(
tempFile: File,
isEmbed: Boolean,
viewModel: KpmViewModel,
snackBarHost: SnackbarHostState,
kpmInstallSuccess: String,
kpmInstallFailed: String
) {
val moduleId = extractModuleId(tempFile.name)
if (moduleId == null) {
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
tempFile.delete()
return
}
val targetPath = "/data/adb/kpm/$moduleId.kpm"
try {
if (isEmbed) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
}
val loadResult = loadKpmModule(tempFile.absolutePath)
if (loadResult.startsWith("Error")) {
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
} else {
viewModel.fetchModuleList()
snackBarHost.showSnackbar(
message = kpmInstallSuccess,
duration = SnackbarDuration.Short
)
}
} catch (e: Exception) {
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
snackBarHost.showSnackbar(
message = kpmInstallFailed,
duration = SnackbarDuration.Short
)
}
tempFile.delete()
}
private fun extractModuleId(fileName: String): String? {
return try {
val decodedFileName = URLDecoder.decode(fileName, "UTF-8")
val pattern = "([^/]*?)\\.kpm$".toRegex()
val matchResult = pattern.find(decodedFileName)
matchResult?.groupValues?.get(1)
} catch (e: Exception) {
Log.e("KsuCli", "Failed to extract module ID: ${e.message}", e)
null
}
}
private suspend fun handleModuleUninstall(
module: KpmViewModel.ModuleInfo,
viewModel: KpmViewModel,
snackBarHost: SnackbarHostState,
kpmUninstallSuccess: String,
kpmUninstallFailed: String,
failedToCheckModuleFile: String,
uninstall: String,
cancel: String,
confirmTitle : String,
confirmContent : String,
confirmDialog: ConfirmDialogHandle
) {
val moduleFileName = "${module.id}.kpm"
globalModuleFileName = moduleFileName
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
val fileExists = try {
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
result
} catch (e: Exception) {
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
snackBarHost.showSnackbar(
message = failedToCheckModuleFile,
duration = SnackbarDuration.Short
)
false
}
val confirmResult = confirmDialog.awaitConfirm(
title = confirmTitle,
content = confirmContent,
confirm = uninstall,
dismiss = cancel
)
if (confirmResult == ConfirmResult.Confirmed) {
try {
val unloadResult = unloadKpmModule(module.id)
if (unloadResult.startsWith("Error")) {
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Short
)
return
}
if (fileExists) {
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
}
viewModel.fetchModuleList()
snackBarHost.showSnackbar(
message = kpmUninstallSuccess,
duration = SnackbarDuration.Short
)
} catch (e: Exception) {
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
snackBarHost.showSnackbar(
message = kpmUninstallFailed,
duration = SnackbarDuration.Short
)
}
}
}
@Composable
private fun KpmModuleItem(
module: KpmViewModel.ModuleInfo,
onUninstall: () -> Unit,
onControl: () -> Unit
) {
val viewModel: KpmViewModel = viewModel()
val scope = rememberCoroutineScope()
val snackBarHost = remember { SnackbarHostState() }
val successMessage = stringResource(R.string.kpm_control_success)
val failureMessage = stringResource(R.string.kpm_control_failed)
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
AlertDialog(
onDismissRequest = { viewModel.hideInputDialog() },
title = { Text(stringResource(R.string.kpm_control)) },
text = {
OutlinedTextField(
value = viewModel.inputArgs,
onValueChange = { viewModel.updateInputArgs(it) },
label = { Text(stringResource(R.string.kpm_args)) },
placeholder = { Text(module.args) }
)
},
confirmButton = {
TextButton(
onClick = {
scope.launch {
val result = viewModel.executeControl()
val message = when (result) {
0 -> successMessage
else -> failureMessage
}
snackBarHost.showSnackbar(message)
onControl()
}
}
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = { viewModel.hideInputDialog() }) {
Text(stringResource(R.string.cancel))
}
}
)
}
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = module.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
style = MaterialTheme.typography.bodyMedium
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = module.description,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilledTonalButton(
onClick = { viewModel.showInputDialog(module.id) },
enabled = module.hasAction
) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = null
)
Text(stringResource(R.string.kpm_control))
}
FilledTonalButton(
onClick = onUninstall
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = null
)
Text(stringResource(R.string.kpm_uninstall))
}
}
}
}
}

View File

@@ -1,88 +0,0 @@
package zako.zako.zako.ui.theme
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
object CardConfig {
val defaultElevation: Dp = 0.dp
var cardAlpha by mutableStateOf(0.45f)
var cardElevation by mutableStateOf(defaultElevation)
var isShadowEnabled by mutableStateOf(true)
var isCustomAlphaSet by mutableStateOf(false)
var isUserDarkModeEnabled by mutableStateOf(false)
var isUserLightModeEnabled by mutableStateOf(false)
var isCustomBackgroundEnabled by mutableStateOf(false)
fun save(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().apply {
putFloat("card_alpha", cardAlpha)
putBoolean("custom_background_enabled", cardElevation == 0.dp)
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
apply()
}
}
fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
}
fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled
cardElevation = if (enabled) defaultElevation else 0.dp
}
fun setDarkModeDefaults() {
if (!isCustomAlphaSet) {
cardAlpha = 0.35f
cardElevation = 0.dp
}
}
}
@Composable
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = when {
CardConfig.isUserLightModeEnabled -> {
Color.Black
}
CardConfig.isUserDarkModeEnabled -> {
Color.White
}
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
Color.Black
}
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
Color.Black
}
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
Color.White
}
else -> {
Color.White
}
}
)
fun getCardElevation() = CardConfig.cardElevation

View File

@@ -1,170 +0,0 @@
package zako.zako.zako.ui.theme
import androidx.compose.ui.graphics.Color
sealed class ThemeColors {
abstract val Primary: Color
abstract val Secondary: Color
abstract val Tertiary: Color
abstract val OnPrimary: Color
abstract val OnSecondary: Color
abstract val OnTertiary: Color
abstract val PrimaryContainer: Color
abstract val SecondaryContainer: Color
abstract val TertiaryContainer: Color
abstract val OnPrimaryContainer: Color
abstract val OnSecondaryContainer: Color
abstract val OnTertiaryContainer: Color
abstract val ButtonContrast: Color
open fun getCustomSliderActiveColor(): Color = Primary
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
// Default Theme (white)
object Default : ThemeColors() {
override val Primary = Color(0xFFFFFFFF)
override val Secondary = Color(0xFFF5F5F5)
override val Tertiary = Color(0xFFE0E0E0)
override val OnPrimary = Color(0xFF616161)
override val OnSecondary = Color(0xFF616161)
override val OnTertiary = Color(0xFF616161)
override val PrimaryContainer = Color(0xFFF5F5F5)
override val SecondaryContainer = Color(0xFFEEEEEE)
override val TertiaryContainer = Color(0xFFE0E0E0)
override val OnPrimaryContainer = Color(0xFF000000)
override val OnSecondaryContainer = Color(0xFF000000)
override val OnTertiaryContainer = Color(0xFF000000)
override val ButtonContrast = Color(0xFF00BFFF)
}
// Blue Theme
object Blue : ThemeColors() {
override val Primary = Color(0xFF2196F3)
override val Secondary = Color(0xFF1E88E5)
override val Tertiary = Color(0xFF0D47A1)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFCBE6FC)
override val SecondaryContainer = Color(0xFFBBDEFB)
override val TertiaryContainer = Color(0xFF90CAF9)
override val OnPrimaryContainer = Color(0xFF0A1A2E)
override val OnSecondaryContainer = Color(0xFF0A192D)
override val OnTertiaryContainer = Color(0xFF071B3D)
override val ButtonContrast = Color(0xFF00BFFF)
}
// Green Theme
object Green : ThemeColors() {
override val Primary = Color(0xFF4CAF50)
override val Secondary = Color(0xFF43A047)
override val Tertiary = Color(0xFF1B5E20)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFC8E6C9)
override val SecondaryContainer = Color(0xFFA5D6A7)
override val TertiaryContainer = Color(0xFF81C784)
override val OnPrimaryContainer = Color(0xFF0A1F0B)
override val OnSecondaryContainer = Color(0xFF0A1D0B)
override val OnTertiaryContainer = Color(0xFF071F09)
override val ButtonContrast = Color(0xFF32CD32)
}
// Purple Theme
object Purple : ThemeColors() {
override val Primary = Color(0xFF9C27B0)
override val Secondary = Color(0xFF8E24AA)
override val Tertiary = Color(0xFF4A148C)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFE1BEE7)
override val SecondaryContainer = Color(0xFFCE93D8)
override val TertiaryContainer = Color(0xFFB39DDB)
override val OnPrimaryContainer = Color(0xFF1F0A23)
override val OnSecondaryContainer = Color(0xFF1C0A21)
override val OnTertiaryContainer = Color(0xFF12071C)
override val ButtonContrast = Color(0xFFDA70D6)
}
// Orange Theme
object Orange : ThemeColors() {
override val Primary = Color(0xFFFF9800)
override val Secondary = Color(0xFFFB8C00)
override val Tertiary = Color(0xFFE65100)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFE0B2)
override val SecondaryContainer = Color(0xFFFFCC80)
override val TertiaryContainer = Color(0xFFFFB74D)
override val OnPrimaryContainer = Color(0xFF1A1100)
override val OnSecondaryContainer = Color(0xFF1A1000)
override val OnTertiaryContainer = Color(0xFF1A0B00)
override val ButtonContrast = Color(0xFFFF6347)
}
// Pink Theme
object Pink : ThemeColors() {
override val Primary = Color(0xFFE91E63)
override val Secondary = Color(0xFFD81B60)
override val Tertiary = Color(0xFF880E4F)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFF8BBD0)
override val SecondaryContainer = Color(0xFFF48FB1)
override val TertiaryContainer = Color(0xFFE91E63)
override val OnPrimaryContainer = Color(0xFF2E0A14)
override val OnSecondaryContainer = Color(0xFF2B0A13)
override val OnTertiaryContainer = Color(0xFF1C0311)
override val ButtonContrast = Color(0xFFFF1493)
}
// Gray Theme
object Gray : ThemeColors() {
override val Primary = Color(0xFF9E9E9E)
override val Secondary = Color(0xFF757575)
override val Tertiary = Color(0xFF616161)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFEEEEEE)
override val SecondaryContainer = Color(0xFFE0E0E0)
override val TertiaryContainer = Color(0xFFBDBDBD)
override val OnPrimaryContainer = Color(0xFF1A1A1A)
override val OnSecondaryContainer = Color(0xFF171717)
override val OnTertiaryContainer = Color(0xFF141414)
override val ButtonContrast = Color(0xFF696969)
}
object Yellow : ThemeColors() {
override val Primary = Color(0xFFFFD700)
override val Secondary = Color(0xFFFFBC52)
override val Tertiary = Color(0xFF795548)
override val OnPrimary = Color(0xFFFFFFFF)
override val OnSecondary = Color(0xFFFFFFFF)
override val OnTertiary = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFF7D6)
override val SecondaryContainer = Color(0xFFFFE6B3)
override val TertiaryContainer = Color(0xFFD7CCC8)
override val OnPrimaryContainer = Color(0xFF1A1600)
override val OnSecondaryContainer = Color(0xFF1A1100)
override val OnTertiaryContainer = Color(0xFF1A1717)
override val ButtonContrast = Color(0xFFFFD700)
}
companion object {
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
"blue" -> Blue
"green" -> Green
"purple" -> Purple
"orange" -> Orange
"pink" -> Pink
"gray" -> Gray
"white" -> Yellow
else -> Default
}
}
}

View File

@@ -1,350 +0,0 @@
package zako.zako.zako.ui.theme
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.zIndex
import coil.compose.rememberAsyncImagePainter
import androidx.compose.foundation.background
import androidx.compose.ui.graphics.luminance
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import androidx.core.content.edit
import androidx.core.net.toUri
object ThemeConfig {
var customBackgroundUri by mutableStateOf<Uri?>(null)
var forceDarkMode by mutableStateOf<Boolean?>(null)
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
var useDynamicColor by mutableStateOf(false)
}
@Composable
private fun getDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
background = Color.Transparent,
surface = Color.Transparent,
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
surfaceVariant = Color(0xFF2F2F2F),
onSurfaceVariant = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
outline = Color.White.copy(alpha = 0.12f),
outlineVariant = Color.White.copy(alpha = 0.12f)
)
@Composable
private fun getLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.Primary,
onPrimary = ThemeConfig.currentTheme.OnPrimary,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
secondary = ThemeConfig.currentTheme.Secondary,
onSecondary = ThemeConfig.currentTheme.OnSecondary,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
tertiary = ThemeConfig.currentTheme.Tertiary,
onTertiary = ThemeConfig.currentTheme.OnTertiary,
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
background = Color.Transparent,
surface = Color.Transparent,
onBackground = Color.Black.copy(alpha = 0.87f),
onSurface = Color.Black.copy(alpha = 0.87f),
surfaceVariant = Color(0xFFF5F5F5),
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
outline = Color.Black.copy(alpha = 0.12f),
outlineVariant = Color.Black.copy(alpha = 0.12f)
)
// 复制图片到应用内部存储
fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
try {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
val fileName = "custom_background.jpg"
val file = File(filesDir, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(4 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
outputStream.flush()
outputStream.close()
inputStream.close()
return Uri.fromFile(file)
} catch (e: Exception) {
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
return null
}
}
@Composable
fun KernelSUTheme(
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
true -> true
false -> false
null -> isSystemInDarkTheme()
},
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
content: @Composable () -> Unit
) {
val context = LocalContext.current
context.loadCustomBackground()
context.loadThemeColors()
context.loadDynamicColorState()
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) {
val originalScheme = dynamicDarkColorScheme(context)
originalScheme.copy(
// 调整按钮相关颜色
primary = adjustColor(originalScheme.primary),
onPrimary = adjustColor(originalScheme.onPrimary),
primaryContainer = adjustColor(originalScheme.primaryContainer),
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
background = Color.Transparent,
surface = Color.Transparent,
onBackground = Color.White,
onSurface = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onSecondaryContainer = Color.White,
onTertiaryContainer = Color.White
)
} else {
val originalScheme = dynamicLightColorScheme(context)
originalScheme.copy(
primary = adjustColor(originalScheme.primary),
onPrimary = adjustColor(originalScheme.onPrimary),
primaryContainer = adjustColor(originalScheme.primaryContainer),
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
background = Color.Transparent,
surface = Color.Transparent
)
}
}
darkTheme -> getDarkColorScheme()
else -> getLightColorScheme()
}
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
if (darkTheme && !dynamicColor) {
CardConfig.setDarkModeDefaults()
}
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
MaterialTheme(
colorScheme = colorScheme,
typography = Typography
) {
Box(modifier = Modifier.fillMaxSize()) {
// 背景图层
ThemeConfig.customBackgroundUri?.let { uri ->
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
) {
// 背景图片
Box(
modifier = Modifier
.fillMaxSize()
.paint(
painter = rememberAsyncImagePainter(
model = uri,
onError = {
ThemeConfig.customBackgroundUri = null
context.saveCustomBackground(null)
}
),
contentScale = ContentScale.Crop
)
)
// 亮度调节层
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) {
Color.Black.copy(alpha = 0.4f)
} else {
Color.White.copy(alpha = 0.1f)
}
)
)
// 边缘渐变遮罩层
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) {
Color.Black.copy(alpha = 0.5f)
} else {
Color.Black.copy(alpha = 0.2f)
}
),
radius = 1200f
)
)
)
}
}
// 内容图层
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f)
) {
content()
}
}
}
}
fun Context.saveCustomBackground(uri: Uri?) {
val newUri = uri?.let { copyImageToInternalStorage(it) }
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", newUri?.toString())
}
ThemeConfig.customBackgroundUri = newUri
}
fun Context.loadCustomBackground() {
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
ThemeConfig.customBackgroundUri = uriString?.toUri()
}
fun Context.saveThemeMode(forceDark: Boolean?) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString(
"theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
}
)
}
ThemeConfig.forceDarkMode = forceDark
}
fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) {
"dark" -> true
"light" -> false
else -> null
}
}
fun Context.saveThemeColors(themeName: String) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = when(themeName) {
"blue" -> ThemeColors.Blue
"green" -> ThemeColors.Green
"purple" -> ThemeColors.Purple
"orange" -> ThemeColors.Orange
"pink" -> ThemeColors.Pink
"gray" -> ThemeColors.Gray
"yellow" -> ThemeColors.Yellow
else -> ThemeColors.Default
}
}
fun Context.loadThemeColors() {
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_colors", "default")
ThemeConfig.currentTheme = when(themeName) {
"blue" -> ThemeColors.Blue
"green" -> ThemeColors.Green
"purple" -> ThemeColors.Purple
"orange" -> ThemeColors.Orange
"pink" -> ThemeColors.Pink
"gray" -> ThemeColors.Gray
"yellow" -> ThemeColors.Yellow
else -> ThemeColors.Default
}
}
fun Context.saveDynamicColorState(enabled: Boolean) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
fun Context.loadDynamicColorState() {
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled
}
private fun adjustColor(color: Color): Color {
val minLuminance = 0.75f
val maxLuminance = 1f
var luminance = color.luminance()
if (luminance < minLuminance) {
luminance = minLuminance
} else if (luminance > maxLuminance) {
luminance = maxLuminance
}
return color.copy(luminance)
}
private fun mixColors(color1: Color, color2: Color, ratio: Float): Color {
val r = (color1.red * ratio + color2.red * (1 - ratio))
val g = (color1.green * ratio + color2.green * (1 - ratio))
val b = (color1.blue * ratio + color2.blue * (1 - ratio))
val a = (color1.alpha * ratio + color2.alpha * (1 - ratio))
return Color(r, g, b, a)
}

View File

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

View File

@@ -1,3 +1,5 @@
libzakozako.so
libzakozakozako.so
libkpmmgr.so
libzako.so
libandroidx.graphics.path.so

View File

@@ -50,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>
<string name="home_support_title">از ما حمایت کنید</string>
<string name="home_support_content">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>
<string name="profile">پروفایل برنامه</string>
<string name="profile_default">پیش‌فرض</string>
<string name="profile_template">قالب</string>
<string name="profile_custom">شخصی سازی شده</string>

View File

@@ -50,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">Pelajari cara instal KernelSU dan menggunakan modul</string>
<string name="home_support_title">Dukung Kami</string>
<string name="home_support_content">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>
<string name="profile">Profil Apl</string>
<string name="profile_default">Bawaan</string>
<string name="profile_template">Templat</string>
<string name="profile_custom">Khusus</string>

View File

@@ -34,10 +34,10 @@
<string name="reboot">再起動</string>
<string name="settings">設定</string>
<string name="reboot_userspace">ソフトリブート</string>
<string name="reboot_recovery">リカバリー再起動</string>
<string name="reboot_bootloader">ブートローダー再起動</string>
<string name="reboot_download">ダウンロードモード再起動</string>
<string name="reboot_edl">EDL 再起動</string>
<string name="reboot_recovery">リカバリー再起動</string>
<string name="reboot_bootloader">ブートローダー再起動</string>
<string name="reboot_download">ダウンロードモード再起動</string>
<string name="reboot_edl">EDL 再起動</string>
<string name="about">アプリについて</string>
<string name="module_uninstall_confirm">モジュール %s をアンインストールしますか?</string>
<string name="module_uninstall_success">%s はアンインストールされました</string>
@@ -72,9 +72,8 @@
<string name="failed_to_update_app_profile">%s のアプリのプロファイルの更新をできませでした</string>
<string name="require_kernel_version" formatted="false">現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください!</string>
<string name="settings_umount_modules_default">デフォルトでモジュールのマウントを解除する</string>
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通デフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
<string name="settings_susfs_toggle">Kprobe フックを非表示にする</string>
<string name="settings_susfs_toggle_summary">KSU によって生成された Kprobe フックを無効化して、代替となる組み込みの非 Kprobe を有効化します。Kprobe をサポートしない 非 GKI カーネルに適用される同等の機能を実装します。</string>
<string name="settings_umount_modules_default_summary">アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>
<string name="settings_susfs_toggle">kprobe フックを無効化</string>
<string name="profile_umount_modules_summary">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>
<string name="profile_selinux_domain">ドメイン</string>
<string name="profile_selinux_rules">ルール</string>
@@ -88,7 +87,7 @@
<string name="failed_to_update_sepolicy">SELinux ルールの更新に失敗しました %s</string>
<string name="module_changelog">変更履歴</string>
<string name="settings_profile_template">アプリプロファイルのテンプレート</string>
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理します</string>
<string name="settings_profile_template_summary">アプリプロファイルのローカルおよびオンラインテンプレートを管理します</string>
<string name="app_profile_template_create">テンプレートの作成</string>
<string name="app_profile_template_edit">テンプレートの編集</string>
<string name="app_profile_template_id">ID</string>
@@ -110,14 +109,14 @@
<string name="app_profile_template_import_empty">クリップボードが空です!</string>
<string name="module_changelog_failed">変更ログの取得に失敗しました: %s</string>
<string name="settings_check_update">更新を確認する</string>
<string name="settings_check_update_summary">アプリを開いたときに更新を自動的に確認します</string>
<string name="settings_check_update_summary">アプリを開いたときに更新を自動的に確認します</string>
<string name="grant_root_failed">root の付与に失敗しました!</string>
<string name="action">アクション</string>
<string name="open">開く</string>
<string name="enable_web_debugging">WebView デバッグを有効化する</string>
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合のみ有効してください</string>
<string name="enable_web_debugging_summary">WebUI のデバッグに使用できます。必要な場合のみ有効してください</string>
<string name="direct_install">直接インストール (推奨)</string>
<string name="select_file">ファイルを選択</string>
<string name="select_file">パッチを適用する必要があるミラーを選択</string>
<string name="install_inactive_slot">非アクティブなスロットにインストール (OTA 後)</string>
<string name="install_inactive_slot_warning">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。
\nこのオプションは、OTA が完了した後にのみ使用してください。
@@ -171,10 +170,9 @@
<string name="allowlist_restore_failed">許可リストの復元に失敗: %1$s</string>
<string name="backup_allowlist">許可リストをバックアップ</string>
<string name="restore_allowlist">許可リストを復元</string>
<string name="settings_custom_background">カスタム背景を設定</string>
<string name="settings_custom_background_summary">カスタム背景を設定します。</string>
<string name="settings_card_manage">カードの管理</string>
<string name="settings_card_alpha">カードのアルファ</string>
<string name="settings_custom_background">カスタムアプリ背景</string>
<string name="settings_custom_background_summary">背景にする画像を選択してください</string>
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
<string name="settings_restore_default">デフォルトに復元</string>
<string name="home_android_version">Android のバージョン</string>
<string name="home_device_model">デバイスモデル</string>
@@ -189,23 +187,24 @@
<string name="selinux_enabled">有効</string>
<string name="selinux_disabled">無効</string>
<string name="simple_mode">シンプルモード</string>
<string name="simple_mode_summary">ON にすると不要なカードを非表示にします</string>
<string name="simple_mode_summary">ON にすると不要なカードを非表示にします</string>
<string name="hide_kernel_kernelsu_version">カーネルのバージョンを非表示にする</string>
<string name="hide_kernel_kernelsu_version_summary">カーネルのバージョンを非表示にします</string>
<string name="hide_kernel_kernelsu_version_summary">カーネルのバージョンを非表示にします</string>
<string name="hide_other_info">その他の情報を非表示にする</string>
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします</string>
<string name="hide_other_info_summary">ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします</string>
<string name="hide_susfs_status">SuSFS ステータスを非表示にする</string>
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
<string name="theme_mode">テーマモード</string>
<string name="hide_susfs_status_summary">ホームページ上の SuSFS ステータス情報を非表示にします</string>
<string name="hide_link_card">リンクカードのステータスを隠す</string>
<string name="hide_link_card_summary">ホームページのリンクカード情報を隠す</string>
<string name="theme_mode">テーマ</string>
<string name="theme_follow_system">システムに従う</string>
<string name="theme_light">ライトカラー</string>
<string name="theme_dark">ダークカラー</string>
<string name="theme_light">ライト</string>
<string name="theme_dark">ダーク</string>
<string name="manual_hook">手動でフック</string>
<string name="dynamic_color_title">ダイナミックカラー</string>
<string name="dynamic_color_summary">システムテーマのダイナミックカラーを使用します</string>
<string name="choose_theme_color">テーマカラーを選択</string>
<string name="color_default">ホワイト</string>
<string name="color_blue">ブルー</string>
<string name="color_default">ブルー</string>
<string name="color_green">グリーン</string>
<string name="color_purple">パープル</string>
<string name="color_orange">オレンジ</string>
@@ -214,7 +213,8 @@
<string name="color_ivory">アイボリー</string>
<string name="flash_option">ブラシの設定</string>
<string name="flash_option_tip">フラッシュするファイルを選択</string>
<string name="horizon_kernel">AnyKernel3 をフラッシュ</string>
<string name="horizon_kernel">AnyKernel3 をインストール</string>
<string name="horizon_kernel_summary">AnyKernel3 カーネルファイルをフラッシュします</string>
<string name="root_required">root 権限が必要です</string>
<string name="copy_failed">ファイルのコピーに失敗しました</string>
<string name="reboot_complete_title">スクラブが完了しました</string>
@@ -222,48 +222,107 @@
<string name="yes">はい</string>
<string name="no">いいえ</string>
<string name="failed_reboot">再起動に失敗しました</string>
<string name="batch_authorization">bulk ライセンス</string>
<string name="batch_authorization">Bulk ライセンス</string>
<string name="batch_cancel_authorization">認証を一括でキャンセル</string>
<string name="backup">バックアップ</string>
<string name="color_yellow">イエロー</string>
<string name="kpm">KPM モジュール</string>
<string name="kpm">カーネルモジュール</string>
<string name="kpm_title">カーネルモジュール</string>
<string name="kpm_empty">カーネルモジュールは現在インストールされていません</string>
<string name="kpm_version">リリース</string>
<string name="kpm_version">バージョン</string>
<string name="kpm_author">作者</string>
<string name="kpm_uninstall">アンインストール</string>
<string name="kpm_uninstall_success">アンインストールに失敗しました</string>
<string name="kpm_uninstall_failed">アンインストールに失敗しました</string>
<string name="kpm_install">インストールを選択</string>
<string name="kpm_install">インストール</string>
<string name="kpm_install_success">KPM モジュールの読み込みに成功しました</string>
<string name="kpm_install_failed">KPM モジュールの読み込みに失敗しました</string>
<string name="kpm_args">KPM パラメータ</string>
<string name="kpm_control">完全</string>
<string name="kpm_args">パラメータ</string>
<string name="kpm_control">実行</string>
<string name="home_kpm_version">KPM のバージョン</string>
<string name="close_notice">閉じる</string>
<string name="kernel_module_notice">以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra の今後にご期待ください</string>
<string name="kpm_control_success">成功</string>
<string name="kpm_control_failed">失敗</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献に繋がるでしょう</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献は引き続き感謝しています</string>
<string name="not_supported">非対応</string>
<string name="supported">対応</string>
<string name="home_kpm_module">KPM モジュール数: %d </string>
<string name="home_kpm_module">"KPM モジュール数: %d "</string>
<string name="kpm_invalid_file">無効な KPM ファイル</string>
<string name="kernel_patched">カーネルはパッチされていません</string>
<string name="kernel_not_enabled">カーネルは設定されていません</string>
<string name="kernel_not_enabled">カーネルは設定です</string>
<string name="custom_settings">カスタム設定</string>
<string name="kpm_install_mode">インストール</string>
<string name="kpm_install_mode">KPM をインストール</string>
<string name="kpm_install_mode_load">読み込む</string>
<string name="kpm_install_mode_embed">埋め込む</string>
<string name="kpm_install_mode_description">モジュールのインストールモードを選択してください:\n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システム恒久的にインストールします</string>
<string name="kpm_install_mode_description">選択してください: %1\$s モジュールのインストールモード \n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システム恒久的にインストールします</string>
<string name="log_failed_to_check_module_file">モジュールファイルの存在を確認できませんでした</string>
<string name="snackbar_failed_to_check_module_file">モジュールファイルが存在するか確認できません</string>
<string name="confirm_uninstall_title">アンインストールを確認</string>
<string name="confirm_uninstall_confirm">アンインストール</string>
<string name="confirm_uninstall_dismiss">中止</string>
<string name="confirm_uninstall_confirm">アンインストール</string>
<string name="confirm_uninstall_dismiss">キャンセル</string>
<string name="theme_color">テーマカラー</string>
<string name="invalid_file_type">ファイルの種類が正しくありません。.kpm ファイルを選択してください。</string>
<string name="invalid_file_type">ファイルの種類が間違っています!.kpm ファイルを選択してください。</string>
<string name="confirm_uninstall_title_with_filename">アンインストール</string>
<string name="confirm_uninstall_content">次の KPM モジュールがアンインストールされます:\n%s</string>
<string name="confirm_uninstall_content">次の KPM がアンインストールされます: %s</string>
<string name="settings_susfs_toggle_summary">KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。</string>
<string name="image_editor_title">背景画像を調整</string>
<string name="image_editor_hint">2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します</string>
<string name="background_image_error">イメージを読み込めません</string>
<string name="reprovision">再プロビジョニング</string>
<!-- Kernel Flash Progress Related -->
<string name="horizon_flash_title">カーネルをフラッシュ</string>
<string name="horizon_logs_label">ログ:</string>
<string name="horizon_flash_complete">フラッシュが完了しました</string>
<!-- Flash Status Related -->
<string name="horizon_preparing">準備中…</string>
<string name="horizon_cleaning_files">ファイルを削除中…</string>
<string name="horizon_copying_files">ファイルをコピー中…</string>
<string name="horizon_extracting_tool">フラッシュツールを展開中…</string>
<string name="horizon_patching_script">フラッシュスクリプトをパッチ中…</string>
<string name="horizon_flashing">カーネルをフラッシュ中…</string>
<string name="horizon_flash_complete_status">フラッシュが完了しました</string>
<!-- Slot selection related strings -->
<string name="select_slot_title">フラッシュ先のスロットを選択</string>
<string name="select_slot_description">フラッシュする boot のターゲットスロットを選択</string>
<string name="slot_a">スロット A</string>
<string name="slot_b">スロット B</string>
<string name="selected_slot">選択したスロット: %1$s</string>
<string name="horizon_getting_original_slot">オリジナルのスロットを取得</string>
<string name="horizon_setting_target_slot">指定するスロットを設定</string>
<string name="horizon_restoring_original_slot">デフォルトのスロットに復元</string>
<string name="current_slot">現在のスロット: %1$s </string>
<!-- Error Messages -->
<string name="horizon_copy_failed">コピーに失敗しました</string>
<string name="horizon_unknown_error">不明なエラー</string>
<string name="flash_failed_message">フラッシュに失敗しました</string>
<!-- lkm/gki install -->
<string name="Lkm_install_methods">LKM の修復またはインストール</string>
<string name="GKI_install_methods">GKI/non-GKI のインストール</string>
<string name="kernel_version_log">カーネルのバージョン: %1$s</string>
<string name="tool_version_log">パッチ適用ツールの使用: %1$s</string>
<string name="configuration">設定</string>
<string name="app_settings">アプリの設定</string>
<string name="tools">ツール</string>
<string name="currently_selected">現在</string>
<!-- String resources used in SuperUser -->
<string name="clear">削除</string>
<string name="apps_with_root">root アプリの権限</string>
<string name="apps_with_custom_profile">カスタマイズされたアプリ構成</string>
<string name="other_apps">その他のアプリ</string>
<string name="no_apps_found">アプリがありません</string>
<string name="selinux_enabled_toast">SELinux 有効</string>
<string name="selinux_disabled_toast">SELinux 無効</string>
<string name="selinux_change_failed">SELinux ステータスの変更に失敗しました</string>
<string name="advanced_settings">高度な設定</string>
<string name="appearance_settings">ツールバーをカスタマイズ</string>
<string name="back">戻る</string>
<string name="expand">最高の状態</string>
<string name="collapse">設置</string>
<string name="susfs_enabled">SuSFS 有効</string>
<string name="susfs_disabled">SuSFS 無効</string>
<string name="background_set_success">背景の設定が成功しました</string>
<string name="background_removed">カスタム背景を削除しました</string>
<string name="root_require_for_install">root 権限が必要</string>
</resources>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">KernelSU</string>
<string name="home">Strona główna</string>
<string name="home_not_installed">Nie zainstalowano</string>
<string name="home_click_to_install">Kliknij, aby zainstalować</string>
@@ -51,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów</string>
<string name="home_support_title">Wesprzyj nas</string>
<string name="home_support_content">KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę.</string>
<string name="profile" translatable="false">Profil aplikacji</string>
<string name="profile_default">Domyślny</string>
<string name="profile_template">Szablon</string>
<string name="profile_custom">Własny</string>

View File

@@ -50,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">Saiba como instalar o KernelSU e usar os módulos</string>
<string name="home_support_title">Apoie-nos</string>
<string name="home_support_content">KernelSU sempre foi e sempre será, gratuito e de código aberto. No entanto, você pode nos ajudar enviando uma pequena doação.</string>
<string name="profile" translatable="false">Perfil do Aplicativo</string>
<string name="profile_default">Padrão</string>
<string name="profile_template">Modelo</string>
<string name="profile_custom">Personalizado</string>

View File

@@ -52,7 +52,6 @@
<string name="home_click_to_learn_kernelsu">Узнайте, как установить KernelSU и использовать модули</string>
<string name="home_support_title">Поддержите нас</string>
<string name="home_support_content">KernelSU был и всегда будет бесплатным и открытым проектом. Однако Вы всегда можете поддержать нас, отправив небольшое пожертвование.</string>
<string name="profile" translatable="false">App Profile</string>
<!--Don't translate this string!-->
<string name="profile_default">По умолчанию</string>
<string name="profile_template">Шаблон</string>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">KernelSU</string>
<string name="home">Ana Sayfa</string>
<string name="home_not_installed">Kurulmadı</string>
<string name="home_click_to_install">Kurmak için tıklayın</string>
@@ -51,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">KernelSU\'nun nasıl kurulacağını ve modüllerin nasıl kullanılacağını öğrenin</string>
<string name="home_support_title">Bizi destekleyin</string>
<string name="home_support_content">KernelSU ücretsiz ve açık kaynaklı bir yazılımdır ve her zaman öyle kalacaktır. Ancak bağış yaparak bize destek olduğunuzu gösterebilirsiniz.</string>
<string name="profile" translatable="false">Uygulama profili</string>
<string name="profile_default">Varsayılan</string>
<string name="profile_template">Şablon</string>
<string name="profile_custom">Özel</string>

View File

@@ -50,7 +50,6 @@
<string name="home_click_to_learn_kernelsu">Дізнайтеся, як інсталювати KernelSU і використовувати модулі</string>
<string name="home_support_title">Підтримати нас</string>
<string name="home_support_content">KernelSU є, і завжди буде безкоштовним та з відкритим кодом. Однак, якщо вам не байдуже, можете зробити невеличке пожертвування.</string>
<string name="profile">Профіль додатка</string>
<string name="profile_default">Типовий</string>
<string name="profile_template">Шаблон</string>
<string name="profile_custom">Власний</string>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">SukiSU Ultra</string>
<string name="home">Home</string>
<string name="home_not_installed">Chưa cài đặt</string>
<string name="home_click_to_install">Nhấn vào đây để cài đặt</string>
@@ -58,7 +57,6 @@
<string name="home_support_title">Hỗ trợ chúng tôi</string>
<string name="home_support_content">KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp.</string>
<string name="about_source_code"><![CDATA[View source code at %1$s<br/>Tham gia kênh %2$s của chúng tôi]]></string>
<string name="profile" translatable="false">Hồ sơ ứng dụng</string>
<string name="profile_default">Mặc định</string>
<string name="profile_template">Bản mẫu</string>
<string name="profile_custom">Tùy chỉnh</string>
@@ -173,7 +171,6 @@
<string name="restore_allowlist">Khôi phục danh sách cho phép</string>
<string name="settings_custom_background">Cài đặt nền tùy chỉnh</string>
<string name="settings_custom_background_summary">Cài đặt tùy chỉnh nền</string>
<string name="settings_card_manage">Quản lý thẻ</string>
<string name="settings_card_alpha">Thẻ alpha</string>
<string name="settings_restore_default">Khôi phục mặc định</string>
<string name="home_android_version">Phiên bản Android</string>
@@ -204,8 +201,7 @@
<string name="dynamic_color_title">Màu sắc động</string>
<string name="dynamic_color_summary">Màu sắc động sử dụng chủ đề hệ thống</string>
<string name="choose_theme_color">Chọn màu chủ đề</string>
<string name="color_default">Trắng</string>
<string name="color_blue">Xanh dương</string>
<string name="color_default">Xanh dương</string>
<string name="color_green">Xanh lá</string>
<string name="color_purple">Tím</string>
<string name="color_orange">Cam</string>
@@ -256,7 +252,7 @@
<string name="kpm_install_mode">Cài đặt</string>
<string name="kpm_install_mode_load">Tải</string>
<string name="kpm_install_mode_embed">Nhúng</string>
<string name="kpm_install_mode_description">Vui lòng chọn chế độ cài đặt mô-đun: \n\nTải: Tải tạm thời mô-đun \nNhúng: Cài đặt vĩnh viễn vào hệ thống</string>
<string name="kpm_install_mode_description">Vui lòng%1\$s chọn chế độ cài đặt mô-đun \n\nTải: Tải tạm thời mô-đun \nNhúng: Cài đặt vĩnh viễn vào hệ thống</string>
<string name="log_failed_to_check_module_file">Không kiểm tra được sự tồn tại của tệp mô-đun</string>
<string name="snackbar_failed_to_check_module_file">Không thể kiểm tra xem tệp mô-đun có tồn tại không</string>
<string name="confirm_uninstall_title">Xác nhận gỡ cài đặt</string>

View File

@@ -117,7 +117,7 @@
<string name="enable_web_debugging">启用 WebView 调试</string>
<string name="enable_web_debugging_summary">可用于调试 WebUI 。请仅在需要时启用。</string>
<string name="direct_install">直接安装(推荐)</string>
<string name="select_file">选择一个文件</string>
<string name="select_file">选择一个需要修补的镜像</string>
<string name="install_inactive_slot">安装到未使用的槽位OTA 后)</string>
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认</string>
<string name="install_next">下一步</string>
@@ -172,8 +172,7 @@
<string name="restore_allowlist">还原应用列表</string>
<string name="settings_custom_background">自定义背景</string>
<string name="settings_custom_background_summary">选择一张图片作为应用背景</string>
<string name="settings_card_manage">卡片管理</string>
<string name="settings_card_alpha">卡片透明度</string>
<string name="settings_card_alpha">卡片不透明度</string>
<string name="settings_restore_default">恢复默认</string>
<string name="home_android_version">Android 版本</string>
<string name="home_device_model">设备</string>
@@ -195,6 +194,8 @@
<string name="hide_other_info_summary">隐藏主页上的超级用户数、模块数和 KPM 模块数信息</string>
<string name="hide_susfs_status">隐藏 SuSFS 状态信息</string>
<string name="hide_susfs_status_summary">隐藏主页上的 SuSFS 状态信息</string>
<string name="hide_link_card">隐藏链接卡片</string>
<string name="hide_link_card_summary">隐藏主页上的链接卡片信息</string>
<string name="theme_mode">主题模式</string>
<string name="theme_follow_system">跟随系统</string>
<string name="theme_light">浅色</string>
@@ -203,8 +204,7 @@
<string name="dynamic_color_title">动态颜色</string>
<string name="dynamic_color_summary">使用系统主题的动态颜色</string>
<string name="choose_theme_color">选择主题色</string>
<string name="color_default"></string>
<string name="color_blue">蓝色</string>
<string name="color_default"></string>
<string name="color_green">绿色</string>
<string name="color_purple">紫色</string>
<string name="color_orange">橙色</string>
@@ -214,6 +214,7 @@
<string name="flash_option">刷入选项</string>
<string name="flash_option_tip">选择要刷入的文件</string>
<string name="horizon_kernel">刷写 AnyKernel3 压缩包</string>
<string name="horizon_kernel_summary">刷入 Anykernel3 内核</string>
<string name="root_required">需要 root 权限</string>
<string name="copy_failed">文件复制失败</string>
<string name="reboot_complete_title">刷写完成</string>
@@ -221,10 +222,9 @@
<string name="yes"></string>
<string name="no"></string>
<string name="failed_reboot">重启失败</string>
<string name="batch_authorization">批量授权</string>
<string name="batch_cancel_authorization">批量取消授权</string>
<string name="batch_authorization">授权</string>
<string name="batch_cancel_authorization">撤销</string>
<string name="color_yellow">黄色</string>
<string name="kpm">内核模块</string>
<string name="kpm_title">内核模块</string>
<string name="kpm_empty">暂无已安装的内核模块</string>
<string name="kpm_version">版本</string>
@@ -236,7 +236,7 @@
<string name="kpm_install_success">加载 kpm 模块成功</string>
<string name="kpm_install_failed">加载 kpm 模块失败</string>
<string name="home_kpm_version">KPM 版本</string>
<string name="kpm_control">执行</string>
<string name="kpm_control">调参</string>
<string name="close_notice">关闭</string>
<string name="kpm_control_success">成功</string>
<string name="kpm_control_failed">错误</string>
@@ -253,7 +253,7 @@
<string name="kpm_install_mode">安装</string>
<string name="kpm_install_mode_load">加载</string>
<string name="kpm_install_mode_embed">嵌入</string>
<string name="kpm_install_mode_description">请选择模块安装模式\n\n加载临时加载模块\n嵌入永久安装到系统</string>
<string name="kpm_install_mode_description">请选择: %1\$s 模块安装模式 \n\n加载临时加载模块\n嵌入永久安装到系统</string>
<string name="snackbar_failed_to_check_module_file">无法检查模块文件是否存在</string>
<string name="confirm_uninstall_title">确认卸载</string>
<string name="confirm_uninstall_confirm">删除</string>
@@ -262,4 +262,62 @@
<string name="invalid_file_type">文件类型不正确,请选择 .kpm 文件</string>
<string name="confirm_uninstall_title_with_filename">卸载</string>
<string name="confirm_uninstall_content">将卸载以下 kpm 模块:\n%s</string>
<string name="image_editor_title">调整背景图片</string>
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
<string name="background_image_error">无法加载图片</string>
<string name="reprovision">重置</string>
<!-- Anykernel3 Kernel刷写进度相关 -->
<string name="horizon_flash_title">刷写Kernel</string>
<string name="horizon_logs_label">日志:</string>
<string name="horizon_flash_complete">刷写完成</string>
<!-- 刷写状态相关 -->
<string name="horizon_preparing">准备中…</string>
<string name="horizon_cleaning_files">清理文件…</string>
<string name="horizon_copying_files">复制文件…</string>
<string name="horizon_extracting_tool">提取刷写工具…</string>
<string name="horizon_patching_script">修补刷写脚本…</string>
<string name="horizon_flashing">刷写内核中…</string>
<string name="horizon_flash_complete_status">刷写完成</string>
<!-- 槽位选择相关字符串 -->
<string name="select_slot_title">选择刷写槽位</string>
<string name="select_slot_description">请选择要刷写boot的目标槽位</string>
<string name="slot_a">A槽位</string>
<string name="slot_b">B槽位</string>
<string name="selected_slot">已选择槽位: %1$s</string>
<string name="horizon_getting_original_slot">获取原有槽位</string>
<string name="horizon_setting_target_slot">设置指定槽位</string>
<string name="horizon_restoring_original_slot">恢复默认槽位</string>
<string name="current_slot">当前槽位:%1$s </string>
<!-- 错误信息 -->
<string name="horizon_copy_failed">复制失败</string>
<string name="horizon_unknown_error">未知错误</string>
<string name="flash_failed_message">刷写失败</string>
<!-- lkm/gki install -->
<string name="Lkm_install_methods">LKM修补/安装</string>
<string name="GKI_install_methods">GKI/non-GKI安装</string>
<string name="kernel_version_log">内核版本:%1$s</string>
<string name="tool_version_log">使用修补工具:%1$s</string>
<string name="configuration">配置</string>
<string name="app_settings">应用设置</string>
<string name="tools">工具</string>
<string name="currently_selected">当前</string>
<!-- SuperUser 里用到的字符串资源 -->
<string name="clear">清除</string>
<string name="apps_with_root">Root 权限应用</string>
<string name="apps_with_custom_profile">自定义配置应用</string>
<string name="other_apps">其他应用</string>
<string name="no_apps_found">未找到应用</string>
<string name="selinux_enabled_toast">SELinux 已设置为启用状态</string>
<string name="selinux_disabled_toast">SELinux 已设置为禁用状态</string>
<string name="selinux_change_failed">SELinux 状态更改失败</string>
<string name="advanced_settings">高级设置</string>
<string name="appearance_settings">外观设置</string>
<string name="back">返回</string>
<string name="expand">展开</string>
<string name="collapse">收起</string>
<string name="susfs_enabled">SuSFS 已启用</string>
<string name="susfs_disabled">SuSFS 已禁用</string>
<string name="background_set_success">背景设置成功</string>
<string name="background_removed">已移除自定义背景</string>
<string name="root_require_for_install">需要 root 权限</string>
</resources>

View File

@@ -12,7 +12,7 @@
<string name="home_unsupported_reason">No KernelSU driver detected on your kernel, wrong kernel?.</string>
<string name="home_kernel">Kernel version</string>
<string name="home_susfs">SuSFS: %s</string>
<string name="home_susfs_version">Version de SuSFS</string>
<string name="home_susfs_version">SuSFS Version</string>
<string name="home_susfs_sus_su">SuS SU</string>
<string name="home_manager_version">Manager version</string>
<string name="home_fingerprint">Fingerprint</string>
@@ -75,8 +75,7 @@
<string name="require_kernel_version" formatted="false">The current KernelSU version %d is too low for the manager to work properly. Please upgrade to version %d or higher!</string>
<string name="settings_umount_modules_default">Umount modules by default</string>
<string name="settings_umount_modules_default_summary">The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set.</string>
<string name="settings_susfs_toggle">Cacher les hooks kprobe</string>
<string name="settings_susfs_toggle_summary">Désactive les hooks kprobe créés par KSU et, à la place, active les hooks non-kprobe intégrés, implémentant les mêmes fonctionnalités qui seraient appliquées à un kernel non-GKI, qui ne supportent krpobe.</string>
<string name="settings_susfs_toggle">Disable kprobe hooks</string>
<string name="profile_umount_modules_summary">Enabling this option will allow KernelSU to restore any modified files by the modules for this app.</string>
<string name="profile_selinux_domain">Domain</string>
<string name="profile_selinux_rules">Rules</string>
@@ -119,7 +118,7 @@
<string name="enable_web_debugging">Enable WebView debugging</string>
<string name="enable_web_debugging_summary">Can be used to debug WebUI. Please enable only when needed.</string>
<string name="direct_install">Direct install (Recommended)</string>
<string name="select_file">Select a file</string>
<string name="select_file">Select a image that needs to be patched</string>
<string name="install_inactive_slot">Install to inactive slot (After OTA)</string>
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
<string name="install_next">Next</string>
@@ -171,24 +170,23 @@
<string name="allowlist_restore_failed">Allowlist restore failed: %1$s</string>
<string name="backup_allowlist">Backup Allowlist</string>
<string name="restore_allowlist">Restore Allowlist</string>
<string name="settings_custom_background">settings custom background</string>
<string name="settings_custom_background_summary">settings custom background summary</string>
<string name="settings_card_manage">card manage</string>
<string name="settings_card_alpha">card alpha</string>
<string name="settings_restore_default">restore default</string>
<string name="settings_custom_background">Custom App Background</string>
<string name="settings_custom_background_summary">Select an image as background</string>
<string name="settings_card_alpha">Navigation bar transparency</string>
<string name="settings_restore_default">Restore default</string>
<string name="home_android_version">Android version</string>
<string name="home_device_model">device model</string>
<string name="home_device_model">Device model</string>
<string name="su_not_allowed">Granting superuser to %s is not allowed</string>
<string name="settings_disable_su">Disable su compatibility</string>
<string name="settings_disable_su_summary">Temporarily disable any applications from obtaining root privileges via the su command (existing root processes will not be affected).</string>
<string name="using_mksu_manager">You are using the SukiSU Beta manager</string>
<string name="module_install_multiple_confirm">Are you sure you want to install the selected %d modules?</string>
<string name="module_install_multiple_confirm_with_names">Sure you want to install the following %1$d modules? \n\n%2$s</string>
<string name="more_settings">more settings</string>
<string name="more_settings">More settings</string>
<string name="selinux">SELinux</string>
<string name="selinux_enabled">Enabled</string>
<string name="selinux_disabled">Disabled</string>
<string name="simple_mode">simplicity mode</string>
<string name="simple_mode">Simplicity mode</string>
<string name="simple_mode_summary">Hides unnecessary cards when turned on</string>
<string name="hide_kernel_kernelsu_version">Hide kernel version</string>
<string name="hide_kernel_kernelsu_version_summary">Hide kernel version</string>
@@ -196,74 +194,134 @@
<string name="hide_other_info_summary">Hides information about the number of super users, modules and KPM modules on the home page</string>
<string name="hide_susfs_status">Hide SuSFS status</string>
<string name="hide_susfs_status_summary">Hide SuSFS status information on the home page</string>
<string name="theme_mode">Theme Mode</string>
<string name="theme_follow_system">follow-up system</string>
<string name="theme_light">light color</string>
<string name="theme_dark">dark colored</string>
<string name="hide_link_card">Hide Link Card Status</string>
<string name="hide_link_card_summary">Hide link card information on the home page</string>
<string name="theme_mode">Theme</string>
<string name="theme_follow_system">Follow system</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<string name="manual_hook">Manual Hook</string>
<string name="dynamic_color_title">Dynamic colours</string>
<string name="dynamic_color_summary">Dynamic colours using system themes</string>
<string name="choose_theme_color">Choose a theme colour</string>
<string name="color_default">White</string>
<string name="color_blue">blue</string>
<string name="color_green">green</string>
<string name="color_purple">purple</string>
<string name="color_orange">orange</string>
<string name="color_pink">pink</string>
<string name="color_gray">gray</string>
<string name="color_ivory">ivory</string>
<string name="color_default">Blue</string>
<string name="color_green">Green</string>
<string name="color_purple">Purple</string>
<string name="color_orange">Orange</string>
<string name="color_pink">Pink</string>
<string name="color_gray">Gray</string>
<string name="color_ivory">Ivory</string>
<string name="flash_option">Brush Options</string>
<string name="flash_option_tip">Select the file to be flashed</string>
<string name="horizon_kernel">Anykernel3 Flash</string>
<string name="horizon_kernel">Install Anykernel3</string>
<string name="horizon_kernel_summary">Flash AnyKernel3 kernel file</string>
<string name="root_required">Requires root privileges</string>
<string name="copy_failed">File Copy Failure</string>
<string name="reboot_complete_title">Scrubbing complete</string>
<string name="reboot_complete_msg">Whether to reboot immediately</string>
<string name="yes">yes</string>
<string name="no">no</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="failed_reboot">Reboot Failed</string>
<string name="batch_authorization">bulk license</string>
<string name="batch_cancel_authorization">Batch cancel authorization</string>
<string name="batch_authorization">empower</string>
<string name="batch_cancel_authorization">withdraw</string>
<string name="backup">Backup</string>
<string name="color_yellow">Yellow</string>
<string name="kpm">kpm module</string>
<string name="kpm_title">kernel module</string>
<string name="kpm_title">KPM</string>
<string name="kpm_empty">No installed kernel modules at this time</string>
<string name="kpm_version">releases</string>
<string name="kpm_author">author</string>
<string name="kpm_uninstall">uninstallation</string>
<string name="kpm_version">Version</string>
<string name="kpm_author">Author</string>
<string name="kpm_uninstall">Uninstall</string>
<string name="kpm_uninstall_success">Uninstalled successfully</string>
<string name="kpm_uninstall_failed">Failed to uninstall</string>
<string name="kpm_install">Select Installation</string>
<string name="kpm_install">Install</string>
<string name="kpm_install_success">Load of kpm module successful</string>
<string name="kpm_install_failed">Load of kpm module failed</string>
<string name="kpm_args">kpm parameters</string>
<string name="kpm_control">fulfillment</string>
<string name="kpm_args">Parameters</string>
<string name="kpm_control">Execute</string>
<string name="home_kpm_version">KPM Version</string>
<string name="close_notice">close</string>
<string name="close_notice">Close</string>
<string name="kernel_module_notice">The following kernel module functions were developed by KernelPatch and modified to include the kernel module functions of SukiSU Ultra</string>
<string name="home_ContributionCard_kernelsu">SukiSU Ultra Look forward to</string>
<string name="kpm_control_success">success</string>
<string name="kpm_control_failed">failed</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but thanks to the official KernelSU and MKSU etc. for their contributions!</string>
<string name="not_supported">unsupported</string>
<string name="supported">supported</string>
<string name="home_kpm_module">Number of KPM modules%d </string>
<string name="kpm_control_success">Success</string>
<string name="kpm_control_failed">Failed</string>
<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra will be a relatively independent branch of KSU in the future, but we still appreciate the official KernelSU and MKSU etc. for their contributions!</string>
<string name="not_supported">Unsupported</string>
<string name="supported">Supported</string>
<string name="home_kpm_module">"Number of KPM modules: %d "</string>
<string name="kpm_invalid_file">Invalid KPM file</string>
<string name="kernel_patched">Kernel not patched</string>
<string name="kernel_not_enabled">Kernel not configured</string>
<string name="custom_settings">Custom settings</string>
<string name="kpm_install_mode">install</string>
<string name="kpm_install_mode">KPM Install</string>
<string name="kpm_install_mode_load">Load</string>
<string name="kpm_install_mode_embed">embed</string>
<string name="kpm_install_mode_description">Please select the module installation mode: \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
<string name="kpm_install_mode_embed">Embed</string>
<string name="kpm_install_mode_description">Please select: %1\$s Module Installation Mode \n\nLoad: Temporarily load the module \nEmbedded: Permanently install into the system</string>
<string name="log_failed_to_check_module_file">Failed to check module file existence</string>
<string name="snackbar_failed_to_check_module_file">Unable to check if module file exists</string>
<string name="confirm_uninstall_title">Confirm uninstallation</string>
<string name="confirm_uninstall_confirm">removing</string>
<string name="confirm_uninstall_dismiss">abolish</string>
<string name="confirm_uninstall_confirm">Uninstall</string>
<string name="confirm_uninstall_dismiss">Cancel</string>
<string name="theme_color">Theme Color</string>
<string name="invalid_file_type">Incorrect file type, select .kpm file</string>
<string name="confirm_uninstall_title_with_filename">uninstallation</string>
<string name="confirm_uninstall_content">The following kpm modules will be uninstalled\n%s</string>
<string name="invalid_file_type">Incorrect file type! Please select .kpm file.</string>
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
<string name="image_editor_title">Adjust background image</string>
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
<string name="background_image_error">Could not load image</string>
<string name="reprovision">Reprovision</string>
<!-- Kernel Flash Progress Related -->
<string name="horizon_flash_title">Kernel Flashing</string>
<string name="horizon_logs_label">Logs:</string>
<string name="horizon_flash_complete">Flash Complete</string>
<!-- Flash Status Related -->
<string name="horizon_preparing">Preparing…</string>
<string name="horizon_cleaning_files">Cleaning files…</string>
<string name="horizon_copying_files">Copying files…</string>
<string name="horizon_extracting_tool">Extracting flash tool…</string>
<string name="horizon_patching_script">Patching flash script…</string>
<string name="horizon_flashing">Flashing kernel…</string>
<string name="horizon_flash_complete_status">Flash completed</string>
<!-- Slot selection related strings -->
<string name="select_slot_title">Select Flash Slot</string>
<string name="select_slot_description">Please select the target slot for flashing boot</string>
<string name="slot_a">Slot A</string>
<string name="slot_b">Slot B</string>
<string name="selected_slot">Selected slot: %1$s</string>
<string name="horizon_getting_original_slot">Getting the original slot</string>
<string name="horizon_setting_target_slot">Setting the specified slot</string>
<string name="horizon_restoring_original_slot">Restore Default Slot</string>
<string name="current_slot">Current Slot%1$s </string>
<!-- Error Messages -->
<string name="horizon_copy_failed">Copy failed</string>
<string name="horizon_unknown_error">Unknown error</string>
<string name="flash_failed_message">Flash failed</string>
<!-- lkm/gki install -->
<string name="Lkm_install_methods">LKM repair/installation</string>
<string name="GKI_install_methods">GKI/non-GKI installation</string>
<string name="kernel_version_log">Kernel version%1$s</string>
<string name="tool_version_log">Using the patching tool%1$s</string>
<string name="configuration">Configure</string>
<string name="app_settings">Application Settings</string>
<string name="tools">Tools</string>
<string name="currently_selected">Currently</string>
<!-- String resources used in SuperUser -->
<string name="clear">Removals</string>
<string name="apps_with_root">Root Application of permissions</string>
<string name="apps_with_custom_profile">Customized Configuration Application</string>
<string name="other_apps">Other Applications</string>
<string name="no_apps_found">Application not found</string>
<string name="selinux_enabled_toast">SELinux Enabled</string>
<string name="selinux_disabled_toast">SELinux Disabled</string>
<string name="selinux_change_failed">SELinux Status change failed</string>
<string name="advanced_settings">Advanced Settings</string>
<string name="appearance_settings">Customize the toolbar</string>
<string name="back">Comeback</string>
<string name="expand">Be in full swing</string>
<string name="collapse">put away</string>
<string name="susfs_enabled">SuSFS enabled</string>
<string name="susfs_disabled">SuSFS disabled</string>
<string name="background_set_success">Background set successfully</string>
<string name="background_removed">Removed custom backgrounds</string>
<string name="root_require_for_install">Requires root privileges</string>
</resources>

View File

@@ -28,8 +28,8 @@ cmaker {
}
val androidMinSdkVersion = 26
val androidTargetSdkVersion = 35
val androidCompileSdkVersion = 35
val androidTargetSdkVersion = 36
val androidCompileSdkVersion = 36
val androidCompileNdkVersion = "28.0.13004108"
val androidSourceCompatibility = JavaVersion.VERSION_21
val androidTargetCompatibility = JavaVersion.VERSION_21

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