Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fd13d9d8d | ||
|
|
4205db6870 | ||
|
|
70f03081a4 | ||
|
|
5ccb779b6a | ||
|
|
b07bc408ce | ||
|
|
7ee1fd63f1 | ||
|
|
3551441e42 | ||
|
|
4a1ab76322 | ||
|
|
2fedb051b8 | ||
|
|
10c35f4baa | ||
|
|
4f82eda003 | ||
|
|
80f89c0241 | ||
|
|
8399f14fad | ||
|
|
c49a66d1af | ||
|
|
d66b390361 | ||
|
|
9c290a8080 | ||
|
|
48efc28e8f | ||
|
|
634adad15c | ||
|
|
4532bab230 | ||
|
|
d3c9b6e739 | ||
|
|
8e4f980db0 | ||
|
|
cfee357ed1 | ||
|
|
9393459b27 | ||
|
|
60af173a7e | ||
|
|
23e2377f87 | ||
|
|
d45ba31849 | ||
|
|
c5705c2d5d | ||
|
|
dfae83cf58 | ||
|
|
cd5ba3ac3c | ||
|
|
2c2698f6bc | ||
|
|
f57fe79c5d | ||
|
|
91ae4c9650 | ||
|
|
01f44dc1d9 | ||
|
|
6e35b88041 | ||
|
|
c9c122d79b | ||
|
|
4bec5ae7b1 | ||
|
|
f9b3478dbb | ||
|
|
561c82de0a | ||
|
|
e96ceb84c9 | ||
|
|
ddbbeafc64 | ||
|
|
285478a778 | ||
|
|
00ffa86705 | ||
|
|
74ec20745c | ||
|
|
b7b995bf73 | ||
|
|
29b7f9e0ad | ||
|
|
00a4c69227 | ||
|
|
9c204496c3 | ||
|
|
519401cf39 | ||
|
|
f69eb5c115 | ||
|
|
82e96f4394 | ||
|
|
8e3db00b9b | ||
|
|
adf299d9f3 | ||
|
|
483a39c7ac | ||
|
|
c83baad6d5 | ||
|
|
2ff3b5ee06 | ||
|
|
b537b51034 | ||
|
|
bfb6ea3613 | ||
|
|
edf7685e9a | ||
|
|
f65f62360a | ||
|
|
af97488d58 | ||
|
|
6b1f73aa3d | ||
|
|
4eeece9559 | ||
|
|
4d7d5547ac | ||
|
|
7b74e70f97 | ||
|
|
d92f8fc8fd | ||
|
|
55f9de2fa9 | ||
|
|
a12b14ef46 | ||
|
|
4ce6ff6286 | ||
|
|
ce3566640c | ||
|
|
a0a9fb01f4 | ||
|
|
e1bd16d94f | ||
|
|
776ae8744c | ||
|
|
9285945e8b | ||
|
|
75e56038ec | ||
|
|
730d58f18b | ||
|
|
67a05e8813 | ||
|
|
e95a469bdb | ||
|
|
2ff122e235 | ||
|
|
2319452306 | ||
|
|
a0752d10c7 | ||
|
|
9110d89d61 | ||
|
|
39d6962320 | ||
|
|
7b314116e9 | ||
|
|
ef4101cbf9 | ||
|
|
85f5459c1d | ||
|
|
97e367aa92 | ||
|
|
7097986cf5 | ||
|
|
d6c8ef3737 | ||
|
|
d7a5e80d34 | ||
|
|
2d9783e3d4 | ||
|
|
9f407a94e3 | ||
|
|
99726a2c4e | ||
|
|
f3675e7f6e | ||
|
|
b84d528d99 | ||
|
|
0aab0c1d6b | ||
|
|
ab2367f7fa | ||
|
|
1bac30930f | ||
|
|
6a9186300b | ||
|
|
e6dea3c29e | ||
|
|
c873ff74cb | ||
|
|
7b6f451cfb | ||
|
|
73dea0b8e7 | ||
|
|
f71d617cb3 | ||
|
|
f0d8e42026 | ||
|
|
5bbd95e821 | ||
|
|
fa060dca58 | ||
|
|
9c7ba5b998 | ||
|
|
061136900a | ||
|
|
6375bf4b7c | ||
|
|
17288c086a | ||
|
|
15747ceaa5 | ||
|
|
675bb20f52 | ||
|
|
ec0b26a174 | ||
|
|
92f6f2f51e | ||
|
|
587e73b449 | ||
|
|
07c9cce4b9 | ||
|
|
1d34ea4995 | ||
|
|
d58ec6952c | ||
|
|
50631aade6 | ||
|
|
6df8f6f5d4 | ||
|
|
4aee26b48e | ||
|
|
3bbe415c7e | ||
|
|
892fa9040f | ||
|
|
cadc123eab | ||
|
|
3a27537648 | ||
|
|
6fa1a5c8b8 | ||
|
|
b772c8ece1 | ||
|
|
c0e839dd8e | ||
|
|
a6ed7befdc | ||
|
|
c210b00d54 | ||
|
|
13b5290598 | ||
|
|
b99516da69 | ||
|
|
fe8b5f2135 | ||
|
|
04e1b9bf77 | ||
|
|
b8aaf918fe | ||
|
|
54925188e8 | ||
|
|
3443e48ef1 | ||
|
|
53b3e84890 | ||
|
|
a5b85bfdad | ||
|
|
2817583e3c | ||
|
|
8a6116b4ec | ||
|
|
6a4270787a | ||
|
|
5457a4772b | ||
|
|
ee4c3bb03b | ||
|
|
dd1d17d2cf | ||
|
|
3c353e8f88 | ||
|
|
d743073309 | ||
|
|
a636911612 | ||
|
|
7a62f91752 | ||
|
|
b551a54c8f | ||
|
|
26d86aa2fe | ||
|
|
6ee9246650 | ||
|
|
1cd96fbdbf | ||
|
|
a030a026b1 | ||
|
|
8bf9cd0bee | ||
|
|
13b1aad4b8 | ||
|
|
916d956ce2 | ||
|
|
87a7650d26 | ||
|
|
3484e187da | ||
|
|
0835f330e2 | ||
|
|
8064472477 | ||
|
|
2281012e33 | ||
|
|
83eaeab1ba | ||
|
|
6405764df3 | ||
|
|
253276a27b | ||
|
|
855a71ac56 | ||
|
|
96dc53977f | ||
|
|
31111e68eb | ||
|
|
ac0de29872 | ||
|
|
9e2b722491 | ||
|
|
59627e6fe2 | ||
|
|
cd0b5fb378 | ||
|
|
48a3c64c7c | ||
|
|
62da804518 | ||
|
|
439b99cc4a | ||
|
|
64f0efc2c0 | ||
|
|
f196bf5b76 | ||
|
|
790968be6a | ||
|
|
83f0f9537f | ||
|
|
68ebfec918 | ||
|
|
8be4dea081 | ||
|
|
cfdbba45c3 | ||
|
|
d408c9f4bf | ||
|
|
8f4c58c4c3 | ||
|
|
7e88e9648f | ||
|
|
4516d136a4 | ||
|
|
1b85dfbed1 | ||
|
|
807ffb419a | ||
|
|
e826f43aed | ||
|
|
d619f5fafc | ||
|
|
b3e2f9b7ff | ||
|
|
99a39c6f52 | ||
|
|
22991e8740 | ||
|
|
7646ecb6f7 | ||
|
|
204db674bb | ||
|
|
99fe6623de | ||
|
|
f1f78d2485 | ||
|
|
b2ae20b796 | ||
|
|
83bd4e9642 | ||
|
|
767349798a | ||
|
|
ae38f4709b | ||
|
|
fc7001a11a | ||
|
|
9924809bdb | ||
|
|
58a4ff94e4 | ||
|
|
29033e9b80 | ||
|
|
ea24daf37c | ||
|
|
ebc16583fb | ||
|
|
2a10b41781 | ||
|
|
d5946047a1 | ||
|
|
4ff46a4911 | ||
|
|
b587216b5e | ||
|
|
245fce167e | ||
|
|
de9b82ffd5 | ||
|
|
e570f402e4 | ||
|
|
9c761b13fa | ||
|
|
cc4b135d20 | ||
|
|
ec5395c787 | ||
|
|
6d60e54a7d | ||
|
|
28aa34c0b6 | ||
|
|
0701967bab | ||
|
|
a76b1eece4 | ||
|
|
8e791c680e | ||
|
|
fc9f2ccf25 | ||
|
|
d4682fb06e | ||
|
|
377ea183a7 | ||
|
|
72361ab8bf | ||
|
|
f708e583c3 | ||
|
|
d753e1dc48 | ||
|
|
315a8a3805 | ||
|
|
129fed9c9f | ||
|
|
0baccb7621 | ||
|
|
842a8aa45a | ||
|
|
d17843479c | ||
|
|
0d70cc8e58 | ||
|
|
4e6cacb206 | ||
|
|
52514ba35b | ||
|
|
4d59ce435e | ||
|
|
b3b7fa6f4d | ||
|
|
c057c16391 | ||
|
|
dee7cc6f2b | ||
|
|
3d0d87cb0c | ||
|
|
6b66d9b3f8 | ||
|
|
a301d94858 | ||
|
|
01199470f2 | ||
|
|
9e7ea19567 | ||
|
|
cdc6a6cb4a |
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@@ -1,5 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: tiann
|
||||
patreon: weishu
|
||||
custom: https://vxposed.com/donate.html
|
||||
open_collective: sukisu-ultra
|
||||
|
||||
|
||||
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Build LKM for KernelSU Local
|
||||
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: 236
|
||||
os_patch_level: 2025-05
|
||||
- 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: 134
|
||||
os_patch_level: 2025-05
|
||||
- version: "android15-6.6"
|
||||
sub_level: 87
|
||||
os_patch_level: 2025-05
|
||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||
uses: ./.github/workflows/gki-kernel-local.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: self-hosted
|
||||
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
|
||||
24
.github/workflows/build-lkm.yml
vendored
24
.github/workflows/build-lkm.yml
vendored
@@ -24,23 +24,23 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- version: "android12-5.10"
|
||||
sub_level: 233
|
||||
os_patch_level: 2025-02
|
||||
sub_level: 237
|
||||
os_patch_level: 2025-06
|
||||
- version: "android13-5.10"
|
||||
sub_level: 234
|
||||
os_patch_level: 2025-03
|
||||
sub_level: 236
|
||||
os_patch_level: 2025-05
|
||||
- version: "android13-5.15"
|
||||
sub_level: 178
|
||||
os_patch_level: 2025-03
|
||||
sub_level: 180
|
||||
os_patch_level: 2025-05
|
||||
- version: "android14-5.15"
|
||||
sub_level: 178
|
||||
os_patch_level: 2025-03
|
||||
sub_level: 180
|
||||
os_patch_level: 2025-05
|
||||
- version: "android14-6.1"
|
||||
sub_level: 129
|
||||
os_patch_level: 2025-04
|
||||
sub_level: 138
|
||||
os_patch_level: 2025-06
|
||||
- version: "android15-6.6"
|
||||
sub_level: 82
|
||||
os_patch_level: 2025-04
|
||||
sub_level: 89
|
||||
os_patch_level: 2025-06
|
||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||
uses: ./.github/workflows/gki-kernel.yml
|
||||
with:
|
||||
|
||||
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
name: Build Manager Manual
|
||||
|
||||
on:
|
||||
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: self-hosted
|
||||
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-local.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-local.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:
|
||||
- target: aarch64-linux-android
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/susfs.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-kpmmgr:
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-linux-android
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/kpmmgr.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-ksud:
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-linux-android
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-linux-android
|
||||
os: ubuntu-latest
|
||||
- target: armv7-linux-androideabi
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/ksud.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
os: ${{ matrix.os }}
|
||||
pack_lkm: true
|
||||
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||
|
||||
build-manager:
|
||||
if: ${{ always() }}
|
||||
needs: build-ksud
|
||||
runs-on: self-hosted
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./manager
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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: Write key
|
||||
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }}
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
|
||||
{
|
||||
echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
|
||||
echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
|
||||
echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
|
||||
echo KEYSTORE_FILE='key.jks'
|
||||
} >> gradle.properties
|
||||
echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks
|
||||
fi
|
||||
|
||||
- name: Download arm64 susfs
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: susfs-aarch64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download arm64 kpmmgr
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: kpmmgr-aarch64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download arm64 ksud
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ksud-aarch64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download x86_64 ksud
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ksud-x86_64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download arm ksud
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ksud-armv7-linux-androideabi
|
||||
path: .
|
||||
|
||||
- name: Copy ksud to app jniLibs
|
||||
run: |
|
||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p app/src/main/jniLibs/x86_64
|
||||
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||
|
||||
- name: Copy kpmmgr to app jniLibs
|
||||
run: |
|
||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||
|
||||
- name: Copy susfs to app jniLibs
|
||||
run: |
|
||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
export ANDROID_HOME=/root/.android/sdk
|
||||
export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
|
||||
{
|
||||
echo 'org.gradle.parallel=true'
|
||||
echo 'org.gradle.vfs.watch=true'
|
||||
echo 'org.gradle.jvmargs=-Xmx2048m'
|
||||
echo 'android.native.buildOutput=verbose'
|
||||
} >> gradle.properties
|
||||
sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties
|
||||
./gradlew clean assembleRelease
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||
with:
|
||||
name: manager
|
||||
path: manager/app/build/outputs/apk/release/*.apk
|
||||
|
||||
- name: Upload mappings
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||
with:
|
||||
name: "mappings"
|
||||
path: "manager/app/build/outputs/mapping/release/"
|
||||
|
||||
- name: Bot session cache
|
||||
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||
id: bot_session_cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: scripts/ksubot.session
|
||||
key: ${{ runner.os }}-bot-session
|
||||
|
||||
- name: Upload to telegram
|
||||
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||
env:
|
||||
CHAT_ID: ${{ vars.CHAT_ID }}
|
||||
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||
MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }}
|
||||
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||
COMMIT_URL: ${{ github.event.head_commit.url }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
TITLE: Manager
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||
export VERSION=$(git rev-list --count HEAD)
|
||||
APK=$(find ./app/build/outputs/apk/release -name "*.apk")
|
||||
python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK
|
||||
fi
|
||||
10
.github/workflows/build-manager.yml
vendored
10
.github/workflows/build-manager.yml
vendored
@@ -119,6 +119,8 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-linux-android
|
||||
os: ubuntu-latest
|
||||
- target: armv7-linux-androideabi
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/ksud.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
@@ -198,12 +200,20 @@ jobs:
|
||||
name: ksud-x86_64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download arm ksud
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ksud-armv7-linux-androideabi
|
||||
path: .
|
||||
|
||||
- name: Copy ksud to app jniLibs
|
||||
run: |
|
||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p app/src/main/jniLibs/x86_64
|
||||
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||
|
||||
- name: Copy kpmmgr to app jniLibs
|
||||
run: |
|
||||
|
||||
40
.github/workflows/crowdin.yml
vendored
Normal file
40
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'manager/app/src/main/res/values/strings.xml'
|
||||
- 'manager/app/src/main/res/values-*/strings.xml'
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Crowdin Action
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
auto_approve_imported: true
|
||||
download_translations: true
|
||||
skip_untranslated_files: false
|
||||
skip_untranslated_strings: true
|
||||
|
||||
create_pull_request: true
|
||||
localization_branch_name: "Crowdin"
|
||||
pull_request_labels: 'enhancement, translation'
|
||||
pull_request_title: 'opt: sync translation from Crowdin'
|
||||
|
||||
config: 'crowdin.yml'
|
||||
crowdin_branch_name: "main"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
name: GKI Kernel Build Local
|
||||
|
||||
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: self-hosted
|
||||
env:
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
CCACHE_HARDLINK: "true"
|
||||
steps:
|
||||
- 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
|
||||
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'
|
||||
mkdir android-kernel && cd android-kernel
|
||||
repo init --depth=1 --u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||
REMOTE_BRANCH=$(git ls-remote https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
||||
6
crowdin.yml
Normal file
6
crowdin.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: 1
|
||||
files:
|
||||
- source: /manager/app/src/main/res/values/strings.xml
|
||||
translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml
|
||||
@@ -1,6 +1,6 @@
|
||||
# SukiSU Ultra
|
||||
|
||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||
**English** | [简体中文](README.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
|
||||
|
||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||
|
||||
@@ -10,67 +10,110 @@ Android device root solution based on [KernelSU](https://github.com/tiann/Kernel
|
||||
>
|
||||
> 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)
|
||||
Using main branching (non-GKI device builds are not supported) (requires manual integration of susfs)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
Use the main branch
|
||||
Using branches that support non-GKI devices (requires manual integration of susfs)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## How to use integrated susfs
|
||||
|
||||
1. Use the susfs-dev branch directly without any patching
|
||||
> [!Note]
|
||||
>
|
||||
> - Due to SuSFS version changes and unpredictability issues
|
||||
> - This susfs-main branch will only merge the latest new version after a full update
|
||||
> - Please keep an eye on the susfs branch to avoid build failures and incompatibilities caused by the various versions
|
||||
|
||||
## KPM support
|
||||
1. Use susfs-main or other susfs-\* branches directly, no need to integrate susfs again (supports non-GKI device builds)
|
||||
|
||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||
```
|
||||
|
||||
## Hook method
|
||||
|
||||
- This method references the hook [method by rsuntk](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES hook:**
|
||||
|
||||
- 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 (v1.5 version is not available at the moment, if you want to use it, please use v1.4 version, or standard KernelSU hooks)
|
||||
|
||||
- Default hook method on Non-GKI kernels.
|
||||
- Need `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## KPM Support
|
||||
|
||||
- Based on KernelPatch, we have removed duplicates of KSU and kept only 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
|
||||
Repository address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
||||
KPM templates: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> 1. `CONFIG_KPM=y` needs to be added.
|
||||
> 2. Non-GKI devices need to add `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` as well.
|
||||
> 3. Some kernel source code below `4.19` also needs to be backport from `4.19` to the header file `set_memory.h`.
|
||||
|
||||
## How to do a system update to retain ROOT
|
||||
|
||||
- After OTA, don't reboot first, go to the manager flashing/patching kernel interface, find `GKI/non_GKI install` and select the Anykernel3 kernel zip file that needs to be flashed, select the slot that is opposite to the current running slot of the system for flashing, and then reboot to retain the GKI mode update (This method is not supported for all non-GKI devices, so please try it yourself. It is the safest way to use TWRP for non-GKI devices.)
|
||||
- Or use LKM mode to install to the unused slot (after OTA).
|
||||
|
||||
## Compatibility Status
|
||||
|
||||
- KernelSU (versions prior to v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+)
|
||||
|
||||
- Older kernels (4.4+) are also compatible, but the kernel must be built manually
|
||||
|
||||
- KernelSU can support 3.x kernels (3.4-3.18) through additional reverse ports
|
||||
|
||||
- Currently supports `arm64-v8a`, `armeabi-v7a (bare)` and some `X86_64`
|
||||
|
||||
## More links
|
||||
|
||||
**If you need to submit a translation for the manager go to** https://crowdin.com/project/SukiSU-Ultra
|
||||
|
||||
Projects compiled based on Sukisu and susfs
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
|
||||
- [More patched GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) including ZRAM patches, KPM, susfs...
|
||||
- [Less patched GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases) only susfs
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## Hook method
|
||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES hook:**
|
||||
- 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
|
||||
### Universal GKI
|
||||
|
||||
Please follow this guide.
|
||||
|
||||
https://kernelsu.org/guide/installation.html
|
||||
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> 1. for devices with GKI 2.0 such as Xiaomi, Redmi, Samsung, etc. (excludes kernel-modified manufacturers such as Meizu, OnePlus, Zenith, and oppo)
|
||||
> 2. Find the GKI build in [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5). Find the device kernel version. Then download it and use TWRP or kernel flashing tool to flash the zip file with AnyKernel3 suffix. Pixel user need use _Less patched GKI_.
|
||||
> 3. The .zip archive without suffix is uncompressed, the gz suffix is the compression used by Tenguet models.
|
||||
|
||||
### 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.
|
||||
@@ -83,11 +126,22 @@ https://kernelsu.org/guide/installation.html
|
||||
4. Bringing back non-GKI/GKI 1.0 support
|
||||
5. More customization
|
||||
6. Support for KPM kernel modules
|
||||
7. Introducing the Manager for SuSFS Configuration and Advanced Features
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. Uninstalling the KernelSU Manager device is stuck. → Uninstall the application with package name com.sony.playmemories.mobile.
|
||||
|
||||
## 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.
|
||||
|
||||
- The images of the files `ic_launcher(?!.*alt.*).*` with anime character emoticons are copyrighted by [五十根大虾仁](https://space.bilibili.com/370927), the Brand Intellectual Property in the images is owned by [明风 OuO](https://space.bilibili.com/274939213), and the vectorization is done by @MiRinChan. Before using these files, in addition to complying with [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt), you also need to comply with the authorization of the two authors to use these artistic contents.
|
||||
|
||||
- Except for the files or directories mentioned above, all other parts are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
## Afdian link
|
||||
- https://afdian.com/a/shirkneko
|
||||
|
||||
## Sponsorship list
|
||||
|
||||
@@ -96,8 +150,8 @@ https://kernelsu.org/guide/installation.html
|
||||
- [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!
|
||||
- [Saksham Singla](https://github.com/TypeFlu) Website provision as well as maintenance
|
||||
- [OukaroMF](https://github.com/OukaroMF) Donation of website domain name
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SukiSU Ultra
|
||||
|
||||
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
||||
**日本語** | [简体中文](README.md) | [English](README-en.md) | [Türkçe](README-tr.md)
|
||||
|
||||
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||
|
||||
@@ -11,22 +11,40 @@
|
||||
>
|
||||
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
|
||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
||||
## 追加する方法
|
||||
|
||||
## 追加方法
|
||||
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
メインブランチを使用 (非 GKI のデバイスのビルドは非対応) (susfs を手動で統合が必要)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
メインブランチを使用する場合
|
||||
非 GKI のデバイスに対応するブランチを使用 (susfs を手動で統合が必要)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## 統合された susfs の使い方
|
||||
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
1. susfs-main または他の susfs-\* ブランチを直接で使用、susfs の統合は不要 (非 GKI デバイスのビルドに対応)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||
```
|
||||
|
||||
## フックの方式
|
||||
|
||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||
|
||||
1. **KPROBES でフック:**
|
||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||
- GKI カーネルのデフォルトとなるフック方式
|
||||
- `CONFIG_KPROBES=y` が必要です
|
||||
|
||||
2. **手動でフック:**
|
||||
- 標準の 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
|
||||
- 非 GKI カーネル用のデフォルトフック方式
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||
|
||||
## KPM に対応
|
||||
|
||||
@@ -37,35 +55,46 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
|
||||
|
||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
> 1. `CONFIG_KPM=y` が必要です。
|
||||
> 2. 非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
|
||||
> 3. いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
|
||||
|
||||
|
||||
## ROOT を保持した状態でのシステムアップデートの方法
|
||||
|
||||
- 始めに OTA 後すぐに再起動せずにマネージャーのカーネルのフラッシュ、パッチのインターフェースを開いて`GKI/非 GKI のインストール`を見つけます。フラッシュする AnyKernel3 の zip ファイルを選択し、フラッシュする実行中のスロットと逆のスロットを選択後に再起動をして GKI モードの更新が保持できます (この方法はすべての非 GKI のデバイスが対応している訳ではないので、自分でお試しください。これは非 GKI のデバイスで TWRP を使用する最も安全な方法です)。
|
||||
- または LKM モードを使用して未使用のスロットにインストールします (OTA後)。
|
||||
|
||||
## 互換性の状態
|
||||
|
||||
- KernelSU (v1.0.0 より前) は Android GKI 2.0 のデバイス (カーネル 5.10 以降) を公式に対応しています。
|
||||
|
||||
- 古いカーネル (4.4 以降) も互換性がありますが、カーネルを手動で再ビルドする必要があります。
|
||||
|
||||
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) で対応可能です。
|
||||
|
||||
- 現在 `arm64-v8a`, `armeabi-v7a (bare)` および一部の `X86_64` に対応しています。
|
||||
|
||||
## その他のリンク
|
||||
|
||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
||||
**マネージャーの翻訳を行う場合** https://crowdin.com/project/SukiSU-Ultra
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [その他パッチ済み GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) ZRAM パッチ、KPM、susfs が含まれています...
|
||||
- [パッチの少ない GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases) susfs のみ
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## フックの方式
|
||||
|
||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||
|
||||
1. **KPROBES フック:**
|
||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||
- GKI カーネルのデフォルトとなるフック方式
|
||||
- `CONFIG_KPROBES=y` が必要です
|
||||
|
||||
2. **手動でフック:**
|
||||
- 標準の 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
|
||||
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||
|
||||
## 使い方
|
||||
|
||||
### GKI
|
||||
### Universal GKI
|
||||
|
||||
このガイドに従ってください。
|
||||
**すべて**参照してください https://kernelsu.org/ja_JP/guide/installation.html
|
||||
|
||||
https://kernelsu.org/ja_JP/guide/installation.html
|
||||
> [!Note]
|
||||
>
|
||||
> 1. Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス向け (Meizu、OnePlus、Zenith、Oppo などカーネルが変更されているメーカーを除く)
|
||||
> 2. GKI のビルドは[その他のリンク](#その他のリンク)から入手できます。デバイスのカーネルバージョンを確認してください。ダウンロード後に TWRP またはカーネルフラッシュツールを使用して AnyKernel3 の接頭辞を持つ zip ファイルをフラッシュしてください。Pixel のユーザーは、パッチの少ない GKI を使用する必要があります。
|
||||
> 3. 接頭辞のない .zip アーカイブは圧縮されていません。.gz の接頭辞は Tenguet モデルで使用される圧縮になります。
|
||||
|
||||
### OnePlus
|
||||
|
||||
@@ -85,29 +114,34 @@ https://kernelsu.org/ja_JP/guide/installation.html
|
||||
5. その他のカスタマイズ
|
||||
6. KPM カーネルモジュールに対応
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
1. KernelSU Manager のアンインストールが停止してしまう → com.sony.playmemories.mobile のアプリをアンインストールしてください。
|
||||
|
||||
## ライセンス
|
||||
|
||||
- “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) のライセンス下にあります。
|
||||
- 「kernel」のディレクトリ内のファイルは [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) のライセンスに基づいています。
|
||||
|
||||
- アニメキャラクターの絵文字を含む `ic_launcher(?!.*alt.*).*` の画像は、[五十根大虾仁](https://space.bilibili.com/370927)が著作権を所有しています。画像に含まれるブランドの知的財産権は[明风 OuO](https://space.bilibili.com/274939213)が所有しています。ベクトル化は @MiRinChan が行っています。これらのファイルを使用する前に[クリエイティブコモンズ 表示 - 非営利 - 継承 4.0 国際](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt)に準拠することに加え、これらの芸術的コンテンツを使用するためには 2 名の著者の許可に従う必要があります。
|
||||
|
||||
## スポンサーシップの一覧
|
||||
|
||||
- [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ありがとう!
|
||||
|
||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||
- [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 の支援に感謝します
|
||||
- [Saksham Singla](https://github.com/TypeFlu) ウェブサイトの提供とメンテナンス
|
||||
- [OukaroMF](https://github.com/OukaroMF) ウェブサイトのドメインと寄付
|
||||
|
||||
## 貢献者
|
||||
|
||||
- [KernelSU](https://github.com/tiann/KernelSU): オリジナルのプロジェクトです。
|
||||
- [MKSU](https://github.com/5ec1cff/KernelSU): 使用しているプロジェクトです。
|
||||
- [RKSU](https://github.com/rsuntk/KernelsU): このプロジェクトのカーネルを使用して非 GKI デバイスのサポートを追加しています。
|
||||
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):使用している susfs ファイルシステムです。
|
||||
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU について。
|
||||
- [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): KernelPatch はカーネルモジュールの APatch 実装での重要な部分となります。
|
||||
- [KernelSU](https://github.com/tiann/KernelSU): オリジナルのプロジェクト
|
||||
- [MKSU](https://github.com/5ec1cff/KernelSU): 使用しているプロジェクト
|
||||
- [RKSU](https://github.com/rsuntk/KernelsU): このプロジェクトのカーネルを使用した非 GKI デバイスのサポートの再導入
|
||||
- [susfs](https://gitlab.com/simonpunk/susfs4ksu): susfs ファイルシステムの使用
|
||||
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU の概念化
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): パワフルな root ユーティリティ
|
||||
- [genuine](https://github.com/brevent/genuine/): APK v2 署名認証
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): いくつかの root キットユーティリティ
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch はカーネルモジュールの APatch 実装の重要な部分での活用
|
||||
|
||||
149
docs/README-tr.md
Normal file
149
docs/README-tr.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# SukiSU Ultra
|
||||
|
||||
**Türkçe** | [简体中文](README.md) | [English](README-en.md) | [日本語](README-ja.md)
|
||||
|
||||
[KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü
|
||||
|
||||
**Deneysel! Kullanım riski size aittir!**
|
||||
|
||||
> Bu resmi olmayan bir daldır, tüm hakları saklıdır [@tiann](https://github.com/tiann)
|
||||
>
|
||||
> Ancak, gelecekte ayrı bir KSU dalı olarak devam edeceğiz
|
||||
|
||||
## Nasıl Eklenir
|
||||
|
||||
Çekirdek kaynak kodunun kök dizininde aşağıdaki komutları çalıştırın:
|
||||
|
||||
Ana dalı kullanın (GKI olmayan cihazlar için desteklenmez)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
GKI olmayan cihazları destekleyen dalı kullanın
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## susfs Nasıl Entegre Edilir
|
||||
|
||||
1. Doğrudan susfs-main veya susfs-* dalını kullanın, susfs entegrasyonuna gerek yok
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||
```
|
||||
|
||||
## Kanca Yöntemleri
|
||||
|
||||
- Bu bölüm [rsuntk\'nin kanca yöntemlerinden](https://github.com/rsuntk/KernelSU) alıntılanmıştır
|
||||
|
||||
1. **KPROBES Kancası:**
|
||||
|
||||
- Yüklenebilir çekirdek modülleri (LKM) için kullanılır
|
||||
- GKI 2.0 çekirdeğinin varsayılan kanca yöntemi
|
||||
- `CONFIG_KPROBES=y` gerektirir
|
||||
|
||||
2. **Manuel Kanca:**
|
||||
- Standart KernelSU kancası: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx\'nin syscall manuel kancası: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- GKI olmayan çekirdeğin varsayılan kanca yöntemi
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` gerektirir
|
||||
|
||||
## KPM Desteği
|
||||
|
||||
- KernelPatch tabanlı olarak KSU ile çakışan işlevleri kaldırdık ve yalnızca KPM desteğini koruduk
|
||||
- APatch ile daha fazla uyumlu fonksiyon ekleyerek KPM işlevlerinin bütünlüğünü sağlayacağız
|
||||
|
||||
Kaynak kodu: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
KPM şablonu: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> 1. `CONFIG_KPM=y` gerektirir
|
||||
> 2. GKI olmayan cihazlar ayrıca `CONFIG_KALLSYMS=y` ve `CONFIG_KALLSYMS_ALL=y` gerektirir
|
||||
> 3. Bazı çekirdek `4.19` altı kaynak kodları, `4.19`dan geri taşınan başlık dosyası `set_memory.h` gerektirir
|
||||
|
||||
## Sistem Güncellemesini Yaparak ROOT\'u Koruma
|
||||
|
||||
- OTA\'dan sonra hemen yeniden başlatmayın, yöneticiye girin ve çekirdek yazma/onarma arayüzüne gidin, `GKI/non_GKI yükleme` seçeneğini bulun ve Anykernel3 çekirdek sıkıştırma dosyasını seçin, şu anda sistemin çalıştığı yuva ile zıt yuvaya yazın ve yeniden başlatın, böylece GKI modu güncellemesini koruyabilirsiniz (şu anda tüm GKI olmayan cihazlar bu yöntemi desteklemiyor, lütfen kendiniz deneyin. GKI olmayan cihazlar için TWRP kullanmak en güvenlidir)
|
||||
- Veya kullanılmayan yuvaya LKM modunu kullanarak yükleyin (OTA\'dan sonra)
|
||||
|
||||
## Uyumluluk Durumu
|
||||
|
||||
- KernelSU (v1.0.0 öncesi sürümler) resmi olarak Android GKI 2.0 cihazlarını destekler (çekirdek 5.10+)
|
||||
|
||||
- Eski çekirdekler (4.4+) de uyumludur, ancak çekirdeği manuel olarak oluşturmanız gerekir
|
||||
|
||||
- Daha fazla geri taşımayla KernelSU, 3.x çekirdeğini (3.4-3.18) destekleyebilir
|
||||
|
||||
- Şu anda `arm64-v8a`, `armeabi-v7a (bare)` ve bazı `X86_64` desteklenmektedir
|
||||
|
||||
## Daha Fazla Bağlantı
|
||||
|
||||
SukiSU ve susfs tabanlı derlenen projeler
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## Kullanım Yöntemi
|
||||
|
||||
### Evrensel GKI
|
||||
|
||||
Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresinden inceleyin
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> 1. Xiaomi, Redmi, Samsung gibi GKI 2.0 cihazlar için uygundur (Meizu, OnePlus, Realme ve Oppo gibi değiştirilmiş çekirdekli üreticiler hariç)
|
||||
> 2. [Daha fazla bağlantı](#daha-fazla-bağlantı) bölümündeki GKI tabanlı projeleri bulun. Cihaz çekirdek sürümünü bulun. Ardından indirin ve TWRP veya çekirdek yazma aracı kullanarak AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||
> 3. Genellikle sonek olmayan .zip sıkıştırılmış paketler sıkıştırılmamıştır, gz soneki olanlar ise Dimensity modelleri için kullanılan sıkıştırma yöntemidir
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. Daha fazla bağlantı bölümündeki OnePlus projesini bulun ve kendiniz doldurun, ardından bulut derleme yapın ve AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> - Çekirdek sürümü için yalnızca ilk iki haneyi doldurmanız yeterlidir, örneğin 5.10, 5.15, 6.1, 6.6
|
||||
> - İşlemci kod adını kendiniz arayın, genellikle tamamen İngilizce ve sayı içermeden oluşur
|
||||
> - Dal ve yapılandırma dosyasını kendiniz OnePlus çekirdek kaynak kodundan doldurun
|
||||
|
||||
## Özellikler
|
||||
|
||||
1. Çekirdek tabanlı `su` ve root erişim yönetimi
|
||||
2. 5ec1cff\'nin [Magic Mount](https://github.com/5ec1cff/KernelSU) tabanlı modül sistemi
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html): root yetkilerini kafeste kilitleyin
|
||||
4. GKI 2.0 olmayan çekirdekler için desteğin geri getirilmesi
|
||||
5. Daha fazla özelleştirme özelliği
|
||||
6. KPM çekirdek modülleri için destek
|
||||
|
||||
## Lisans
|
||||
|
||||
- `kernel` dizinindeki dosyalar [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.
|
||||
- Anime karakter ifadeleri içeren `ic_launcher(?!.*alt.*).*` dosyalarının görüntüleri [五十根大虾仁](https://space.bilibili.com/370927) tarafından telif hakkıyla korunmaktadır, görüntülerdeki Marka Fikri Mülkiyeti [明风 OuO](https://space.bilibili.com/274939213)'ye aittir ve vektörleştirme @MiRinChan tarafından yapılmıştır. Bu dosyaları kullanmadan önce, [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) ile uyumlu olmanın yanı sıra, bu sanatsal içerikleri kullanmak için iki yazarın yetkilendirmesine de uymanız gerekir.
|
||||
- Yukarıda belirtilen dosyalar veya dizinler hariç, diğer tüm parçalar [GPL-3.0 veya üzeri](https://www.gnu.org/licenses/gpl-3.0.html)'dir.
|
||||
|
||||
## Afdian Bağlantısı
|
||||
|
||||
- https://afdian.com/a/shirkneko
|
||||
|
||||
## Sponsor Listesi
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) Bana sağladığınız destek için çok teşekkür ederim
|
||||
- [zaoqi123](https://github.com/zaoqi123) Bana sütlü çay ısmarlamanız da güzel
|
||||
- [wswzgdg](https://github.com/wswzgdg) Bu projeye olan desteğiniz için çok teşekkür ederim
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) Çok teşekkür ederim
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 100 USDT için teşekkürler
|
||||
|
||||
## Katkıda Bulunanlar
|
||||
|
||||
- [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje
|
||||
- [MKSU](https://github.com/5ec1cff/KernelSU): Kullanılan proje
|
||||
- [RKSU](https://github.com/rsuntk/KernelsU): GKI olmayan cihazlar için destek sağlayan proje
|
||||
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu): Kullanılan susfs dosya sistemi
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): Güçlü root aracı
|
||||
- [genuine](https://github.com/brevent/genuine/): APK v2 imza doğrulama
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): Bazı rootkit becerileri
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch, APatch\'in çekirdek modüllerini uygulamak için kritik bir parçadır
|
||||
@@ -1,12 +1,12 @@
|
||||
# SukiSU Ultra
|
||||
|
||||
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
||||
**简体中文** | [English](README-en.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
|
||||
|
||||
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
||||
|
||||
**实验性! 使用风险自负!**
|
||||
|
||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||
> 这是非官方分支,[@tiann](https://github.com/tiann) 有权保留所有权利
|
||||
>
|
||||
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||
|
||||
@@ -14,34 +14,49 @@
|
||||
|
||||
在内核源码的根目录下执行以下命令:
|
||||
|
||||
使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持)
|
||||
使用 main 分支 (不支持非 GKI 设备构建) (需要手动集成 susfs)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
使用 main 分支
|
||||
使用支持非 GKI 设备的分支 (需要手动集成 susfs)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## 如何集成 susfs
|
||||
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||
1. 直接使用 susfs-main 或者其他 susfs-\* 分支,不需要再集成 susfs (支持非 GKI 设备构建)
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> - 因 SuSFS 版本的变化和不可测问题
|
||||
> - 本 susfs-main 分支只在完整更新后再合并最新新版本
|
||||
> - 请随时留意 susfs 分支的变化情况以免导致构建失败以及各种版本导致的不兼容问题
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||
```
|
||||
|
||||
## 钩子方法
|
||||
|
||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES 钩子:**
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
|
||||
2. **手动钩子:**
|
||||
- 标准的 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
|
||||
- 非 GKI 内核的默认挂钩方法
|
||||
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
- 标准的 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 (v1.5 版本暂不可用,如要使用请使用 v1.4 版本,或者标准 KernelSU 钩子)
|
||||
|
||||
- 非 GKI 内核的默认挂钩方法
|
||||
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## KPM 支持
|
||||
|
||||
@@ -52,10 +67,35 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
||||
|
||||
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> 1. 需要 `CONFIG_KPM=y`
|
||||
> 2. 非 GKI 设备还需要 `CONFIG_KALLSYMS=y` 和 `CONFIG_KALLSYMS_ALL=y`
|
||||
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h`
|
||||
|
||||
## 如何进行系统更新保留 ROOT
|
||||
|
||||
- OTA 后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的 Anykernel3 内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留 GKI 模式更新(暂不支持所有非 GKI 设备使用这种方法,请自行尝试。非 GKI 设备使用 TWRP 刷写是最稳妥的)
|
||||
- 或者使用 LKM 模式的安装到未使用的槽位(OTA 后)
|
||||
|
||||
## 兼容状态
|
||||
|
||||
- KernelSU(v1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+)
|
||||
|
||||
- 旧内核(4.4+)也兼容,但必须手动构建内核
|
||||
|
||||
- 通过更多的反向移植,KernelSU 可以支持 3.x 内核(3.4-3.18)
|
||||
|
||||
- 目前支持 `arm64-v8a` ,`armeabi-v7a (bare)` 和部分 `X86_64`
|
||||
|
||||
## 更多链接
|
||||
|
||||
**如果你需要为管理器提交翻译请前往** https://crowdin.com/project/SukiSU-Ultra
|
||||
|
||||
基于 SukiSU 和 susfs 编译的项目
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
|
||||
- [增强 GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)(包括 ZRAM 算法等补丁、KPM、susfs 等)
|
||||
- [GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases)(若增强 GKI boot 失败再尝试这份,这份没有 KPM 等修改,只有 susfs)
|
||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## 使用方法
|
||||
@@ -65,16 +105,17 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
请**全部**参考 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 后缀的压缩包即可
|
||||
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用 TWRP 或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可。Pixel 请使用不是增强的 GKI。
|
||||
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||
|
||||
|
||||
### 一加
|
||||
|
||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||
@@ -87,11 +128,21 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
4. 恢复对非 GKI 2.0 内核的支持
|
||||
5. 更多自定义功能
|
||||
6. 对 KPM 内核模块的支持
|
||||
7. 引入SuSFS配置的管理器以及进阶功能
|
||||
|
||||
## 疑难解答
|
||||
|
||||
1. 卸载 KernelSU 管理器设备卡死。→ 卸载包名为 com.sony.playmemories.mobile 的应用。
|
||||
|
||||
## 许可证
|
||||
|
||||
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
- 有动漫人物图片表情包的这些文件 `ic_launcher(?!.*alt.*).*` 的图像版权为[五十根大虾仁](https://space.bilibili.com/370927)所有,图像中的 Brand Intellectual Property 由[明风 OuO](https://space.bilibili.com/274939213)所有,矢量化由 @MiRinChan 完成,在使用这些文件之前,除了必须遵守 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) 以外,还需要遵守向前两者索要使用这些艺术内容的授权。
|
||||
- 除了以上所述的文件或目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
|
||||
## 爱发电链接
|
||||
|
||||
- https://afdian.com/a/shirkneko
|
||||
|
||||
## 赞助名单
|
||||
|
||||
@@ -100,14 +151,14 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||
|
||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||
- [Saksham Singla](https://github.com/TypeFlu) 网站的提供以及维护
|
||||
- [OukaroMF](https://github.com/OukaroMF) 网站域名捐赠
|
||||
|
||||
## 贡献
|
||||
|
||||
- [KernelSU](https://github.com/tiann/KernelSU):原始项目
|
||||
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
|
||||
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非GKI设备重新进行支持
|
||||
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非 GKI 设备重新进行支持
|
||||
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的构想
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
||||
|
||||
@@ -16,20 +16,14 @@ config KSU_DEBUG
|
||||
help
|
||||
Enable KernelSU debug mode.
|
||||
|
||||
config KSU_HOOK
|
||||
bool "Enable KernelSU Hook"
|
||||
default n
|
||||
help
|
||||
This option enables the KernelSU Hook feature. If enabled, it will
|
||||
override the kernel version check and enable the hook functionality.
|
||||
|
||||
config KPM
|
||||
bool "Enable SukiSU KPM"
|
||||
depends on KSU && 64BIT
|
||||
default n
|
||||
help
|
||||
Enabling this option will activate the KPM feature of SukiSU.
|
||||
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||
but it may affect system stability.
|
||||
|
||||
|
||||
select KALLSYMS
|
||||
select KALLSYMS_ALL
|
||||
endmenu
|
||||
|
||||
@@ -19,19 +19,57 @@ obj-$(CONFIG_KSU) += kernelsu.o
|
||||
obj-$(CONFIG_KPM) += kpm/
|
||||
|
||||
|
||||
# .git is a text file while the module is imported by 'git submodule add'.
|
||||
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
||||
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
|
||||
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 -- 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 KernelSU a git submodule!")
|
||||
ccflags-y += -DKSU_VERSION=16
|
||||
REPO_OWNER := SukiSU-Ultra
|
||||
REPO_NAME := SukiSU-Ultra
|
||||
REPO_BRANCH := main
|
||||
KSU_VERSION_API := 3.1.7
|
||||
|
||||
GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git
|
||||
CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl
|
||||
|
||||
KSU_GITHUB_VERSION := $(shell $(CURL_BIN) -s "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
|
||||
KSU_GITHUB_VERSION_COMMIT := $(shell $(CURL_BIN) -sI "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/commits?sha=$(REPO_BRANCH)&per_page=1" | grep -i "link:" | sed -n 's/.*page=\([0-9]*\)>; rel="last".*/\1/p')
|
||||
|
||||
LOCAL_GIT_EXISTS := $(shell test -e $(srctree)/$(src)/../.git && echo 1 || echo 0)
|
||||
|
||||
define get_ksu_version_full
|
||||
v$1-$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --abbrev-ref HEAD)
|
||||
endef
|
||||
|
||||
ifeq ($(KSU_GITHUB_VERSION_COMMIT),)
|
||||
ifeq ($(LOCAL_GIT_EXISTS),1)
|
||||
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||
KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count $(REPO_BRANCH))
|
||||
KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700)
|
||||
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION))
|
||||
else
|
||||
KSU_VERSION := 13000
|
||||
$(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION))
|
||||
endif
|
||||
else
|
||||
KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION_COMMIT) + 700)
|
||||
$(info -- $(REPO_NAME) version (GitHub): $(KSU_VERSION))
|
||||
endif
|
||||
|
||||
ifeq ($(KSU_GITHUB_VERSION),)
|
||||
ifeq ($(LOCAL_GIT_EXISTS),1)
|
||||
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_VERSION_API))
|
||||
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION_FULL))
|
||||
$(info -- $(REPO_NAME) Formatted version (local .git): $(KSU_VERSION))
|
||||
else
|
||||
KSU_VERSION_FULL := v$(KSU_VERSION_API)-$(REPO_NAME)-unknown@unknown
|
||||
$(warning -- $(REPO_NAME) version: $(KSU_VERSION_FULL))
|
||||
endif
|
||||
else
|
||||
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_GITHUB_VERSION))
|
||||
$(info -- $(REPO_NAME) version (Github): $(KSU_VERSION_FULL))
|
||||
endif
|
||||
|
||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||
ccflags-y += -DKSU_VERSION_FULL=\"$(KSU_VERSION_FULL)\"
|
||||
|
||||
ifndef KSU_EXPECTED_SIZE
|
||||
KSU_EXPECTED_SIZE := 0x35c
|
||||
endif
|
||||
|
||||
@@ -110,6 +110,7 @@ static void setup_groups(struct root_profile *profile, struct cred *cred)
|
||||
|
||||
groups_sort(group_info);
|
||||
set_groups(cred, group_info);
|
||||
put_group_info(group_info);
|
||||
}
|
||||
|
||||
static void disable_seccomp()
|
||||
@@ -134,18 +135,18 @@ void escape_to_root(void)
|
||||
{
|
||||
struct cred *cred;
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
do {
|
||||
cred = (struct cred *)__task_cred((current));
|
||||
BUG_ON(!cred);
|
||||
} while (!get_cred_rcu(cred));
|
||||
cred = prepare_creds();
|
||||
if (!cred) {
|
||||
pr_warn("prepare_creds failed!\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cred->euid.val == 0) {
|
||||
pr_warn("Already root, don't escape!\n");
|
||||
rcu_read_unlock();
|
||||
abort_creds(cred);
|
||||
return;
|
||||
}
|
||||
|
||||
struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
|
||||
|
||||
cred->uid.val = profile->uid;
|
||||
@@ -176,7 +177,7 @@ void escape_to_root(void)
|
||||
|
||||
setup_groups(profile, cred);
|
||||
|
||||
rcu_read_unlock();
|
||||
commit_creds(cred);
|
||||
|
||||
// Refer to kernel/seccomp.c: seccomp_set_mode_strict
|
||||
// When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.
|
||||
@@ -226,6 +227,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_EXT4_FS
|
||||
static void nuke_ext4_sysfs() {
|
||||
struct path path;
|
||||
int err = kern_path("/data/adb/modules", 0, &path);
|
||||
@@ -242,7 +244,11 @@ static void nuke_ext4_sysfs() {
|
||||
}
|
||||
|
||||
ext4_unregister_sysfs(sb);
|
||||
path_put(&path);
|
||||
}
|
||||
#else
|
||||
static inline void nuke_ext4_sysfs() { }
|
||||
#endif
|
||||
|
||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
unsigned long arg4, unsigned long arg5)
|
||||
@@ -302,7 +308,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
if (copy_to_user(arg3, &version, sizeof(version))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
u32 version_flags = 0;
|
||||
u32 version_flags = 2;
|
||||
#ifdef MODULE
|
||||
version_flags |= 0x1;
|
||||
#endif
|
||||
@@ -313,6 +319,21 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Allow root manager to get full version strings
|
||||
if (arg2 == CMD_GET_FULL_VERSION) {
|
||||
char ksu_version_full[KSU_FULL_VERSION_STRING] = {0};
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
|
||||
strscpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
|
||||
#else
|
||||
strlcpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
|
||||
#endif
|
||||
if (copy_to_user((void __user *)arg3, ksu_version_full, KSU_FULL_VERSION_STRING)) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
return -EFAULT;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_REPORT_EVENT) {
|
||||
if (!from_root) {
|
||||
return 0;
|
||||
@@ -425,6 +446,13 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
}
|
||||
#endif
|
||||
|
||||
if (arg2 == CMD_ENABLE_KPM) {
|
||||
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
|
||||
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
|
||||
pr_info("KPM: copy_to_user() failed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// all other cmds are for 'root manager'
|
||||
if (!from_manager) {
|
||||
return 0;
|
||||
@@ -553,11 +581,13 @@ static void try_umount(const char *mnt, bool check_mnt, int flags)
|
||||
|
||||
if (path.dentry != path.mnt->mnt_root) {
|
||||
// it is not root mountpoint, maybe umounted by others already.
|
||||
path_put(&path);
|
||||
return;
|
||||
}
|
||||
|
||||
// we are only interest in some specific mounts
|
||||
if (check_mnt && !should_umount(&path)) {
|
||||
path_put(&path);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,13 @@ uid_t sukisu_get_manager_uid() {
|
||||
return ksu_manager_uid;
|
||||
}
|
||||
|
||||
static
|
||||
void sukisu_set_manager_uid(uid_t uid, int force) {
|
||||
if(force || ksu_manager_uid == -1) {
|
||||
ksu_manager_uid = uid;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
|
||||
struct CompactAddressSymbol {
|
||||
@@ -75,7 +82,8 @@ static struct CompactAddressSymbol address_symbol [] = {
|
||||
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
|
||||
{ "is_uid_should_umount", &sukisu_is_uid_should_umount },
|
||||
{ "is_current_uid_manager", &sukisu_is_current_uid_manager },
|
||||
{ "get_manager_uid", &sukisu_get_manager_uid }
|
||||
{ "get_manager_uid", &sukisu_get_manager_uid },
|
||||
{ "sukisu_set_manager_uid", &sukisu_set_manager_uid }
|
||||
};
|
||||
|
||||
unsigned long sukisu_compact_find_symbol(const char* name) {
|
||||
|
||||
@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
|
||||
DEFINE_MEMBER(task_struct, group_leader)
|
||||
DEFINE_MEMBER(task_struct, mm)
|
||||
DEFINE_MEMBER(task_struct, active_mm)
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
|
||||
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
|
||||
#else
|
||||
DEFINE_MEMBER(task_struct, thread_pid)
|
||||
#endif
|
||||
DEFINE_MEMBER(task_struct, files)
|
||||
DEFINE_MEMBER(task_struct, seccomp)
|
||||
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||
|
||||
10
kernel/ksu.h
10
kernel/ksu.h
@@ -24,6 +24,10 @@
|
||||
#define CMD_IS_SU_ENABLED 14
|
||||
#define CMD_ENABLE_SU 15
|
||||
|
||||
#define CMD_GET_FULL_VERSION 30
|
||||
|
||||
#define CMD_ENABLE_KPM 100
|
||||
|
||||
#define EVENT_POST_FS_DATA 1
|
||||
#define EVENT_BOOT_COMPLETED 2
|
||||
#define EVENT_MODULE_MOUNTED 3
|
||||
@@ -34,6 +38,12 @@
|
||||
#define KSU_MAX_GROUPS 32
|
||||
#define KSU_SELINUX_DOMAIN 64
|
||||
|
||||
// SukiSU Ultra kernel su version full strings
|
||||
#ifndef KSU_VERSION_FULL
|
||||
#define KSU_VERSION_FULL "v3.x-00000000@unknown"
|
||||
#endif
|
||||
#define KSU_FULL_VERSION_STRING 255
|
||||
|
||||
struct root_profile {
|
||||
int32_t uid;
|
||||
int32_t gid;
|
||||
|
||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
||||
// Detect whether it is on or not
|
||||
static bool is_boot_phase = true;
|
||||
|
||||
#ifdef CONFIG_COMPAT
|
||||
bool ksu_is_compat __read_mostly = false;
|
||||
#endif
|
||||
|
||||
void on_post_fs_data(void)
|
||||
{
|
||||
static bool done = false;
|
||||
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
|
||||
if (get_user(compat, argv.ptr.compat + nr))
|
||||
return ERR_PTR(-EFAULT);
|
||||
|
||||
ksu_is_compat = true;
|
||||
return compat_ptr(compat);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
||||
#define CMD_TYPE_CHANGE 8
|
||||
#define CMD_GENFSCON 9
|
||||
|
||||
#ifdef CONFIG_64BIT
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
char __user *sepol1;
|
||||
char __user *sepol2;
|
||||
char __user *sepol3;
|
||||
char __user *sepol4;
|
||||
char __user *sepol5;
|
||||
char __user *sepol6;
|
||||
char __user *sepol7;
|
||||
u64 field_sepol1;
|
||||
u64 field_sepol2;
|
||||
u64 field_sepol3;
|
||||
u64 field_sepol4;
|
||||
u64 field_sepol5;
|
||||
u64 field_sepol6;
|
||||
u64 field_sepol7;
|
||||
};
|
||||
#ifdef CONFIG_COMPAT
|
||||
extern bool ksu_is_compat __read_mostly;
|
||||
struct sepol_compat_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
u32 field_sepol1;
|
||||
u32 field_sepol2;
|
||||
u32 field_sepol3;
|
||||
u32 field_sepol4;
|
||||
u32 field_sepol5;
|
||||
u32 field_sepol6;
|
||||
u32 field_sepol7;
|
||||
};
|
||||
#endif // CONFIG_COMPAT
|
||||
#else
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
u32 field_sepol1;
|
||||
u32 field_sepol2;
|
||||
u32 field_sepol3;
|
||||
u32 field_sepol4;
|
||||
u32 field_sepol5;
|
||||
u32 field_sepol6;
|
||||
u32 field_sepol7;
|
||||
};
|
||||
#endif // CONFIG_64BIT
|
||||
|
||||
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||
char **object)
|
||||
@@ -192,14 +220,58 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
pr_info("SELinux permissive or disabled when handle policy!\n");
|
||||
}
|
||||
|
||||
u32 cmd, subcmd;
|
||||
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
|
||||
|
||||
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
|
||||
if (unlikely(ksu_is_compat)) {
|
||||
struct sepol_compat_data compat_data;
|
||||
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
sepol1 = compat_ptr(compat_data.field_sepol1);
|
||||
sepol2 = compat_ptr(compat_data.field_sepol2);
|
||||
sepol3 = compat_ptr(compat_data.field_sepol3);
|
||||
sepol4 = compat_ptr(compat_data.field_sepol4);
|
||||
sepol5 = compat_ptr(compat_data.field_sepol5);
|
||||
sepol6 = compat_ptr(compat_data.field_sepol6);
|
||||
sepol7 = compat_ptr(compat_data.field_sepol7);
|
||||
cmd = compat_data.cmd;
|
||||
subcmd = compat_data.subcmd;
|
||||
} else {
|
||||
struct sepol_data data;
|
||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
sepol1 = data.field_sepol1;
|
||||
sepol2 = data.field_sepol2;
|
||||
sepol3 = data.field_sepol3;
|
||||
sepol4 = data.field_sepol4;
|
||||
sepol5 = data.field_sepol5;
|
||||
sepol6 = data.field_sepol6;
|
||||
sepol7 = data.field_sepol7;
|
||||
cmd = data.cmd;
|
||||
subcmd = data.subcmd;
|
||||
}
|
||||
#else
|
||||
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
|
||||
struct sepol_data data;
|
||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
u32 cmd = data.cmd;
|
||||
u32 subcmd = data.subcmd;
|
||||
sepol1 = data.field_sepol1;
|
||||
sepol2 = data.field_sepol2;
|
||||
sepol3 = data.field_sepol3;
|
||||
sepol4 = data.field_sepol4;
|
||||
sepol5 = data.field_sepol5;
|
||||
sepol6 = data.field_sepol6;
|
||||
sepol7 = data.field_sepol7;
|
||||
cmd = data.cmd;
|
||||
subcmd = data.subcmd;
|
||||
#endif
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_buf[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c, *p;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
||||
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm failed.\n");
|
||||
goto exit;
|
||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_set[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(operation, data.sepol4,
|
||||
if (strncpy_from_user(operation, sepol4,
|
||||
sizeof(operation)) < 0) {
|
||||
pr_err("sepol: copy operation failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
||||
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm_set failed.\n");
|
||||
goto exit;
|
||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_TYPE_STATE) {
|
||||
char src[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char type[MAX_SEPOL_LEN];
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
|
||||
if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
|
||||
pr_err("sepol: copy type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
|
||||
if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_ATTR) {
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
|
||||
if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
char object[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
char *real_object;
|
||||
if (data.sepol5 == NULL) {
|
||||
if (sepol5 == NULL) {
|
||||
real_object = NULL;
|
||||
} else {
|
||||
if (strncpy_from_user(object, data.sepol5,
|
||||
if (strncpy_from_user(object, sepol5,
|
||||
sizeof(object)) < 0) {
|
||||
pr_err("sepol: copy object failed.\n");
|
||||
goto exit;
|
||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char cls[MAX_SEPOL_LEN];
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char name[MAX_SEPOL_LEN];
|
||||
char path[MAX_SEPOL_LEN];
|
||||
char context[MAX_SEPOL_LEN];
|
||||
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
|
||||
if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
|
||||
pr_err("sepol: copy name failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
|
||||
if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
|
||||
pr_err("sepol: copy path failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
||||
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||
0) {
|
||||
pr_err("sepol: copy context failed.\n");
|
||||
goto exit;
|
||||
|
||||
@@ -41,7 +41,7 @@ setup_kernelsu() {
|
||||
echo "[+] Setting up KernelSU..."
|
||||
# Clone the repository and rename it to KernelSU
|
||||
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
||||
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra
|
||||
git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra SukiSU-Ultra
|
||||
mv SukiSU-Ultra KernelSU
|
||||
echo "[+] Repository cloned and renamed to KernelSU."
|
||||
fi
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
extern void escape_to_root();
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
static bool ksu_sucompat_non_kp __read_mostly = true;
|
||||
#endif
|
||||
|
||||
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
||||
{
|
||||
/* To avoid having to mmap a page in userspace, just write below the stack
|
||||
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
||||
{
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
|
||||
// const char sh[] = SH_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
||||
const char sh[] = KSUD_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_ptr))
|
||||
return 0;
|
||||
|
||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
||||
const char su[] = SU_PATH;
|
||||
char path[sizeof(su) + 1];
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp){
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_user))
|
||||
return 0;
|
||||
|
||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
||||
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
||||
#else
|
||||
ksu_sucompat_non_kp = true;
|
||||
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
||||
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
||||
destroy_kprobe(&su_kps[i]);
|
||||
}
|
||||
#else
|
||||
ksu_sucompat_non_kp = false;
|
||||
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
int i, stop = 0;
|
||||
struct list_head data_path_list;
|
||||
INIT_LIST_HEAD(&data_path_list);
|
||||
unsigned long data_app_magic = 0;
|
||||
|
||||
// Initialize APK cache list
|
||||
struct apk_path_hash *pos, *n;
|
||||
@@ -246,6 +247,24 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
goto skip_iterate;
|
||||
}
|
||||
|
||||
// grab magic on first folder, which is /data/app
|
||||
if (!data_app_magic) {
|
||||
if (file->f_inode->i_sb->s_magic) {
|
||||
data_app_magic = file->f_inode->i_sb->s_magic;
|
||||
pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic);
|
||||
} else {
|
||||
filp_close(file, NULL);
|
||||
goto skip_iterate;
|
||||
}
|
||||
}
|
||||
|
||||
if (file->f_inode->i_sb->s_magic != data_app_magic) {
|
||||
pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath,
|
||||
file->f_inode->i_sb->s_magic, data_app_magic);
|
||||
filp_close(file, NULL);
|
||||
goto skip_iterate;
|
||||
}
|
||||
|
||||
iterate_dir(file, &ctx.ctx);
|
||||
filp_close(file, NULL);
|
||||
}
|
||||
@@ -358,12 +377,14 @@ void track_throne()
|
||||
if (ksu_is_manager_uid_valid()) {
|
||||
pr_info("manager is uninstalled, invalidate it!\n");
|
||||
ksu_invalidate_manager_uid();
|
||||
goto prune;
|
||||
}
|
||||
pr_info("Searching manager...\n");
|
||||
search_manager("/data/app", 2, &uid_list);
|
||||
pr_info("Search manager finished\n");
|
||||
}
|
||||
|
||||
prune:
|
||||
// then prune the allowlist
|
||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||
out:
|
||||
|
||||
@@ -50,7 +50,6 @@ android {
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
prefab = true
|
||||
@@ -109,6 +108,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.gson)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
@@ -154,4 +154,9 @@ dependencies {
|
||||
|
||||
implementation(libs.com.github.topjohnwu.libsu.core)
|
||||
|
||||
implementation(libs.mmrl.platform)
|
||||
compileOnly(libs.mmrl.hidden.api)
|
||||
implementation(libs.mmrl.webui)
|
||||
implementation(libs.mmrl.ui)
|
||||
|
||||
}
|
||||
47
manager/app/proguard-rules.pro
vendored
47
manager/app/proguard-rules.pro
vendored
@@ -0,0 +1,47 @@
|
||||
-verbose
|
||||
-optimizationpasses 5
|
||||
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn kotlinx.serialization.**
|
||||
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn com.google.auto.service.AutoService
|
||||
-dontwarn com.google.j2objc.annotations.RetainedWith
|
||||
-dontwarn javax.lang.model.SourceVersion
|
||||
-dontwarn javax.lang.model.element.AnnotationMirror
|
||||
-dontwarn javax.lang.model.element.AnnotationValue
|
||||
-dontwarn javax.lang.model.element.Element
|
||||
-dontwarn javax.lang.model.element.ElementKind
|
||||
-dontwarn javax.lang.model.element.ElementVisitor
|
||||
-dontwarn javax.lang.model.element.ExecutableElement
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
-dontwarn javax.lang.model.element.Name
|
||||
-dontwarn javax.lang.model.element.PackageElement
|
||||
-dontwarn javax.lang.model.element.TypeElement
|
||||
-dontwarn javax.lang.model.element.TypeParameterElement
|
||||
-dontwarn javax.lang.model.element.VariableElement
|
||||
-dontwarn javax.lang.model.type.ArrayType
|
||||
-dontwarn javax.lang.model.type.DeclaredType
|
||||
-dontwarn javax.lang.model.type.ExecutableType
|
||||
-dontwarn javax.lang.model.type.TypeKind
|
||||
-dontwarn javax.lang.model.type.TypeMirror
|
||||
-dontwarn javax.lang.model.type.TypeVariable
|
||||
-dontwarn javax.lang.model.type.TypeVisitor
|
||||
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
|
||||
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.ElementFilter
|
||||
-dontwarn javax.lang.model.util.Elements
|
||||
-dontwarn javax.lang.model.util.SimpleElementVisitor8
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.Types
|
||||
-dontwarn javax.tools.Diagnostic$Kind
|
||||
|
||||
|
||||
# MMRL:webui reflection
|
||||
-keep class com.dergoogler.mmrl.webui.model.ModId { *; }
|
||||
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
|
||||
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }
|
||||
|
||||
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
|
||||
@@ -32,6 +32,19 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".ui.MainActivityAlias"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/ic_launcher_alt"
|
||||
android:roundIcon="@mipmap/ic_launcher_alt_round"
|
||||
android:targetActivity=".ui.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".ui.webui.WebUIActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
@@ -39,6 +52,13 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webui.WebUIXActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.sukisu.zako;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
interface IKsuInterface {
|
||||
ParcelableListSlice<PackageInfo> getPackages(int flags);
|
||||
}
|
||||
BIN
manager/app/src/main/assets/ksu_susfs_1.5.7
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_1.5.7
Normal file
Binary file not shown.
BIN
manager/app/src/main/assets/ksu_susfs_1.5.8
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_1.5.8
Normal file
Binary file not shown.
@@ -1,4 +1,3 @@
|
||||
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||
|
||||
@@ -7,14 +6,11 @@ cmake_minimum_required(VERSION 3.18.1)
|
||||
|
||||
project("kernelsu")
|
||||
|
||||
find_package(cxx REQUIRED CONFIG)
|
||||
link_libraries(cxx::cxx)
|
||||
|
||||
add_library(zako
|
||||
SHARED
|
||||
jni.cc
|
||||
ksu.cc
|
||||
)
|
||||
jni.c
|
||||
ksu.c
|
||||
)
|
||||
|
||||
find_library(log-lib log)
|
||||
|
||||
|
||||
354
manager/app/src/main/cpp/jni.c
Normal file
354
manager/app/src/main/cpp/jni.c
Normal file
@@ -0,0 +1,354 @@
|
||||
#include "prelude.h"
|
||||
#include "ksu.h"
|
||||
|
||||
#include <jni.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
NativeBridge(becomeManager, jboolean, jstring pkg) {
|
||||
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE);
|
||||
bool result = become_manager(cpkg);
|
||||
|
||||
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
|
||||
return result;
|
||||
}
|
||||
|
||||
NativeBridgeNP(getVersion, jint) {
|
||||
return get_version();
|
||||
}
|
||||
|
||||
NativeBridgeNP(getAllowList, jintArray) {
|
||||
int uids[1024];
|
||||
int size = 0;
|
||||
bool result = get_allow_list(uids, &size);
|
||||
|
||||
LogDebug("getAllowList: %d, size: %d", result, size);
|
||||
|
||||
if (result) {
|
||||
jintArray array = GetEnvironment()->NewIntArray(env, size);
|
||||
GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
return GetEnvironment()->NewIntArray(env, 0);
|
||||
}
|
||||
|
||||
NativeBridgeNP(isSafeMode, jboolean) {
|
||||
return is_safe_mode();
|
||||
}
|
||||
|
||||
NativeBridgeNP(isLkmMode, jboolean) {
|
||||
return is_lkm_mode();
|
||||
}
|
||||
|
||||
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
|
||||
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
|
||||
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]);
|
||||
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
|
||||
}
|
||||
}
|
||||
|
||||
static void addIntToList(JNIEnv *env, jobject list, int ele) {
|
||||
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
|
||||
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
|
||||
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele);
|
||||
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
|
||||
}
|
||||
|
||||
static uint64_t capListToBits(JNIEnv *env, jobject list) {
|
||||
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
|
||||
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
|
||||
jint listSize = GetEnvironment()->CallIntMethod(env, list, size);
|
||||
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
|
||||
uint64_t result = 0;
|
||||
for (int i = 0; i < listSize; ++i) {
|
||||
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
|
||||
int data = GetEnvironment()->CallIntMethod(env, integer, intValue);
|
||||
|
||||
if (cap_valid(data)) {
|
||||
result |= (1ULL << data);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int getListSize(JNIEnv *env, jobject list) {
|
||||
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
|
||||
return GetEnvironment()->CallIntMethod(env, list, size);
|
||||
}
|
||||
|
||||
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
|
||||
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
|
||||
data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue);
|
||||
}
|
||||
}
|
||||
|
||||
NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) {
|
||||
if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char key[KSU_MAX_PACKAGE_NAME] = { 0 };
|
||||
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr);
|
||||
strcpy(key, cpkg);
|
||||
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
|
||||
|
||||
struct app_profile profile = { 0 };
|
||||
profile.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(profile.key, key);
|
||||
profile.current_uid = uid;
|
||||
|
||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||
|
||||
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
|
||||
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
|
||||
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
|
||||
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
|
||||
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
|
||||
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
|
||||
|
||||
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
|
||||
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
|
||||
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
|
||||
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
|
||||
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
|
||||
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
|
||||
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
|
||||
|
||||
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
|
||||
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
|
||||
|
||||
GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key));
|
||||
GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid);
|
||||
|
||||
if (useDefaultProfile) {
|
||||
// no profile found, so just use default profile:
|
||||
// don't allow root and use default profile!
|
||||
LogDebug("use default profile for: %s, %d", key, uid);
|
||||
|
||||
// allow_su = false
|
||||
// non root use default = true
|
||||
GetEnvironment()->SetBooleanField(env, obj, allowSuField, false);
|
||||
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
bool allowSu = profile.allow_su;
|
||||
|
||||
if (allowSu) {
|
||||
GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
|
||||
if (strlen(profile.rp_config.template_name) > 0) {
|
||||
GetEnvironment()->SetObjectField(env, obj, rootTemplateField,
|
||||
GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name));
|
||||
}
|
||||
|
||||
GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid);
|
||||
GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid);
|
||||
|
||||
jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField);
|
||||
int groupCount = profile.rp_config.profile.groups_count;
|
||||
if (groupCount > KSU_MAX_GROUPS) {
|
||||
LogDebug("kernel group count too large: %d???", groupCount);
|
||||
groupCount = KSU_MAX_GROUPS;
|
||||
}
|
||||
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
|
||||
|
||||
jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField);
|
||||
for (int i = 0; i <= CAP_LAST_CAP; i++) {
|
||||
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
|
||||
addIntToList(env, capList, i);
|
||||
}
|
||||
}
|
||||
|
||||
GetEnvironment()->SetObjectField(env, obj, domainField,
|
||||
GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain));
|
||||
GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces);
|
||||
GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su);
|
||||
} else {
|
||||
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default);
|
||||
GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
NativeBridge(setAppProfile, jboolean, jobject profile) {
|
||||
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
|
||||
|
||||
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
|
||||
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
|
||||
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
|
||||
|
||||
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
|
||||
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
|
||||
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
|
||||
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
|
||||
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
|
||||
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
|
||||
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
|
||||
|
||||
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
|
||||
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
|
||||
|
||||
jobject key = GetEnvironment()->GetObjectField(env, profile, keyField);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr);
|
||||
char p_key[KSU_MAX_PACKAGE_NAME] = { 0 };
|
||||
strcpy(p_key, cpkg);
|
||||
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg);
|
||||
|
||||
jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField);
|
||||
|
||||
jint uid = GetEnvironment()->GetIntField(env, profile, uidField);
|
||||
jint gid = GetEnvironment()->GetIntField(env, profile, gidField);
|
||||
jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField);
|
||||
jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField);
|
||||
jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField);
|
||||
jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField);
|
||||
jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField);
|
||||
|
||||
struct app_profile p = { 0 };
|
||||
p.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(p.key, p_key);
|
||||
p.allow_su = allowSu;
|
||||
p.current_uid = currentUid;
|
||||
|
||||
if (allowSu) {
|
||||
p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField);
|
||||
jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField);
|
||||
if (templateName) {
|
||||
const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr);
|
||||
strcpy(p.rp_config.template_name, ctemplateName);
|
||||
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName);
|
||||
}
|
||||
|
||||
p.rp_config.profile.uid = uid;
|
||||
p.rp_config.profile.gid = gid;
|
||||
|
||||
int groups_count = getListSize(env, groups);
|
||||
if (groups_count > KSU_MAX_GROUPS) {
|
||||
LogDebug("groups count too large: %d", groups_count);
|
||||
return false;
|
||||
}
|
||||
p.rp_config.profile.groups_count = groups_count;
|
||||
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
|
||||
|
||||
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
|
||||
|
||||
const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr);
|
||||
strcpy(p.rp_config.profile.selinux_domain, cdomain);
|
||||
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain);
|
||||
|
||||
p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField);
|
||||
} else {
|
||||
p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField);
|
||||
p.nrp_config.profile.umount_modules = umountModules;
|
||||
}
|
||||
|
||||
return set_app_profile(&p);
|
||||
}
|
||||
|
||||
NativeBridge(uidShouldUmount, jboolean, jint uid) {
|
||||
return uid_should_umount(uid);
|
||||
}
|
||||
|
||||
NativeBridgeNP(isSuEnabled, jboolean) {
|
||||
return is_su_enabled();
|
||||
}
|
||||
|
||||
NativeBridge(setSuEnabled, jboolean, jboolean enabled) {
|
||||
return set_su_enabled(enabled);
|
||||
}
|
||||
|
||||
NativeBridgeNP(isKPMEnabled, jboolean) {
|
||||
return is_KPM_enable();
|
||||
}
|
||||
|
||||
NativeBridgeNP(getHookType, jstring) {
|
||||
char hook_type[16];
|
||||
get_hook_type(hook_type, sizeof(hook_type));
|
||||
return GetEnvironment()->NewStringUTF(env, hook_type);
|
||||
}
|
||||
|
||||
NativeBridgeNP(getSusfsFeatureStatus, jobject) {
|
||||
struct susfs_feature_status status;
|
||||
bool result = get_susfs_feature_status(&status);
|
||||
|
||||
if (!result) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus");
|
||||
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
|
||||
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
|
||||
|
||||
// 设置各个字段
|
||||
jfieldID statusSusPathField = GetEnvironment()->GetFieldID(env, cls, "statusSusPath", "Z");
|
||||
jfieldID statusSusMountField = GetEnvironment()->GetFieldID(env, cls, "statusSusMount", "Z");
|
||||
jfieldID statusAutoDefaultMountField = GetEnvironment()->GetFieldID(env, cls, "statusAutoDefaultMount", "Z");
|
||||
jfieldID statusAutoBindMountField = GetEnvironment()->GetFieldID(env, cls, "statusAutoBindMount", "Z");
|
||||
jfieldID statusSusKstatField = GetEnvironment()->GetFieldID(env, cls, "statusSusKstat", "Z");
|
||||
jfieldID statusTryUmountField = GetEnvironment()->GetFieldID(env, cls, "statusTryUmount", "Z");
|
||||
jfieldID statusAutoTryUmountBindField = GetEnvironment()->GetFieldID(env, cls, "statusAutoTryUmountBind", "Z");
|
||||
jfieldID statusSpoofUnameField = GetEnvironment()->GetFieldID(env, cls, "statusSpoofUname", "Z");
|
||||
jfieldID statusEnableLogField = GetEnvironment()->GetFieldID(env, cls, "statusEnableLog", "Z");
|
||||
jfieldID statusHideSymbolsField = GetEnvironment()->GetFieldID(env, cls, "statusHideSymbols", "Z");
|
||||
jfieldID statusSpoofCmdlineField = GetEnvironment()->GetFieldID(env, cls, "statusSpoofCmdline", "Z");
|
||||
jfieldID statusOpenRedirectField = GetEnvironment()->GetFieldID(env, cls, "statusOpenRedirect", "Z");
|
||||
jfieldID statusMagicMountField = GetEnvironment()->GetFieldID(env, cls, "statusMagicMount", "Z");
|
||||
jfieldID statusSusSuField = GetEnvironment()->GetFieldID(env, cls, "statusSusSu", "Z");
|
||||
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSusPathField, status.status_sus_path);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSusMountField, status.status_sus_mount);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusAutoDefaultMountField, status.status_auto_default_mount);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusAutoBindMountField, status.status_auto_bind_mount);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSusKstatField, status.status_sus_kstat);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusTryUmountField, status.status_try_umount);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusAutoTryUmountBindField, status.status_auto_try_umount_bind);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSpoofUnameField, status.status_spoof_uname);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusEnableLogField, status.status_enable_log);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusHideSymbolsField, status.status_hide_symbols);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSpoofCmdlineField, status.status_spoof_cmdline);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusOpenRedirectField, status.status_open_redirect);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusMagicMountField, status.status_magic_mount);
|
||||
GetEnvironment()->SetBooleanField(env, obj, statusSusSuField, status.status_sus_su);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
NativeBridgeNP(getFullVersion, jstring) {
|
||||
char buff[255] = { 0 };
|
||||
get_full_version((char *) &buff);
|
||||
return GetEnvironment()->NewStringUTF(env, buff);
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
#include <jni.h>
|
||||
|
||||
#include <sys/prctl.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "ksu.h"
|
||||
|
||||
#define LOG_TAG "KernelSU"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
return get_version();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
int uids[1024];
|
||||
int size = 0;
|
||||
bool result = get_allow_list(uids, &size);
|
||||
LOGD("getAllowList: %d, size: %d", result, size);
|
||||
if (result) {
|
||||
auto array = env->NewIntArray(size);
|
||||
env->SetIntArrayRegion(array, 0, size, uids);
|
||||
return array;
|
||||
}
|
||||
return env->NewIntArray(0);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
return is_safe_mode();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
return is_lkm_mode();
|
||||
}
|
||||
|
||||
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto integer = env->NewObject(integerCls, constructor, data[i]);
|
||||
env->CallBooleanMethod(list, add, integer);
|
||||
}
|
||||
}
|
||||
|
||||
static void addIntToList(JNIEnv *env, jobject list, int ele) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
||||
auto integer = env->NewObject(integerCls, constructor, ele);
|
||||
env->CallBooleanMethod(list, add, integer);
|
||||
}
|
||||
|
||||
static uint64_t capListToBits(JNIEnv *env, jobject list) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
||||
auto size = env->GetMethodID(cls, "size", "()I");
|
||||
auto listSize = env->CallIntMethod(list, size);
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
||||
uint64_t result = 0;
|
||||
for (int i = 0; i < listSize; ++i) {
|
||||
auto integer = env->CallObjectMethod(list, get, i);
|
||||
int data = env->CallIntMethod(integer, intValue);
|
||||
|
||||
if (cap_valid(data)) {
|
||||
result |= (1ULL << data);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int getListSize(JNIEnv *env, jobject list) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto size = env->GetMethodID(cls, "size", "()I");
|
||||
return env->CallIntMethod(list, size);
|
||||
}
|
||||
|
||||
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto integer = env->CallObjectMethod(list, get, i);
|
||||
data[i] = env->CallIntMethod(integer, intValue);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
p_key_t key = {};
|
||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||
strcpy(key, cpkg);
|
||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||
|
||||
app_profile profile = {};
|
||||
profile.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(profile.key, key);
|
||||
profile.current_uid = uid;
|
||||
|
||||
bool useDefaultProfile = !get_app_profile(key, &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;");
|
||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
||||
|
||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
||||
|
||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
||||
|
||||
env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key));
|
||||
env->SetIntField(obj, currentUidField, profile.current_uid);
|
||||
|
||||
if (useDefaultProfile) {
|
||||
// no profile found, so just use default profile:
|
||||
// don't allow root and use default profile!
|
||||
LOGD("use default profile for: %s, %d", key, uid);
|
||||
|
||||
// allow_su = false
|
||||
// non root use default = true
|
||||
env->SetBooleanField(obj, allowSuField, false);
|
||||
env->SetBooleanField(obj, nonRootUseDefaultField, true);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
auto allowSu = profile.allow_su;
|
||||
|
||||
if (allowSu) {
|
||||
env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
|
||||
if (strlen(profile.rp_config.template_name) > 0) {
|
||||
env->SetObjectField(obj, rootTemplateField,
|
||||
env->NewStringUTF(profile.rp_config.template_name));
|
||||
}
|
||||
|
||||
env->SetIntField(obj, uidField, profile.rp_config.profile.uid);
|
||||
env->SetIntField(obj, gidField, profile.rp_config.profile.gid);
|
||||
|
||||
jobject groupList = env->GetObjectField(obj, groupsField);
|
||||
int groupCount = profile.rp_config.profile.groups_count;
|
||||
if (groupCount > KSU_MAX_GROUPS) {
|
||||
LOGD("kernel group count too large: %d???", groupCount);
|
||||
groupCount = KSU_MAX_GROUPS;
|
||||
}
|
||||
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
|
||||
|
||||
jobject capList = env->GetObjectField(obj, capabilitiesField);
|
||||
for (int i = 0; i <= CAP_LAST_CAP; i++) {
|
||||
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
|
||||
addIntToList(env, capList, i);
|
||||
}
|
||||
}
|
||||
|
||||
env->SetObjectField(obj, domainField,
|
||||
env->NewStringUTF(profile.rp_config.profile.selinux_domain));
|
||||
env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces);
|
||||
env->SetBooleanField(obj, allowSuField, profile.allow_su);
|
||||
} else {
|
||||
env->SetBooleanField(obj, nonRootUseDefaultField,
|
||||
(jboolean) profile.nrp_config.use_default);
|
||||
env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
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");
|
||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
||||
|
||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
||||
|
||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
||||
|
||||
auto key = env->GetObjectField(profile, keyField);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cpkg = env->GetStringUTFChars((jstring) key, nullptr);
|
||||
p_key_t p_key = {};
|
||||
strcpy(p_key, cpkg);
|
||||
env->ReleaseStringUTFChars((jstring) key, cpkg);
|
||||
|
||||
auto currentUid = env->GetIntField(profile, currentUidField);
|
||||
|
||||
auto uid = env->GetIntField(profile, uidField);
|
||||
auto gid = env->GetIntField(profile, gidField);
|
||||
auto groups = env->GetObjectField(profile, groupsField);
|
||||
auto capabilities = env->GetObjectField(profile, capabilitiesField);
|
||||
auto domain = env->GetObjectField(profile, domainField);
|
||||
auto allowSu = env->GetBooleanField(profile, allowSuField);
|
||||
auto umountModules = env->GetBooleanField(profile, umountModulesField);
|
||||
|
||||
app_profile p = {};
|
||||
p.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(p.key, p_key);
|
||||
p.allow_su = allowSu;
|
||||
p.current_uid = currentUid;
|
||||
|
||||
if (allowSu) {
|
||||
p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField);
|
||||
auto templateName = env->GetObjectField(profile, rootTemplateField);
|
||||
if (templateName) {
|
||||
auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr);
|
||||
strcpy(p.rp_config.template_name, ctemplateName);
|
||||
env->ReleaseStringUTFChars((jstring) templateName, ctemplateName);
|
||||
}
|
||||
|
||||
p.rp_config.profile.uid = uid;
|
||||
p.rp_config.profile.gid = gid;
|
||||
|
||||
int groups_count = getListSize(env, groups);
|
||||
if (groups_count > KSU_MAX_GROUPS) {
|
||||
LOGD("groups count too large: %d", groups_count);
|
||||
return false;
|
||||
}
|
||||
p.rp_config.profile.groups_count = groups_count;
|
||||
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
|
||||
|
||||
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
|
||||
|
||||
auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr);
|
||||
strcpy(p.rp_config.profile.selinux_domain, cdomain);
|
||||
env->ReleaseStringUTFChars((jstring) domain, cdomain);
|
||||
|
||||
p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField);
|
||||
} else {
|
||||
p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField);
|
||||
p.nrp_config.profile.umount_modules = umountModules;
|
||||
}
|
||||
|
||||
return set_app_profile(&p);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
return uid_should_umount(uid);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||
return is_su_enabled();
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||
return set_su_enabled(enabled);
|
||||
}
|
||||
142
manager/app/src/main/cpp/ksu.c
Normal file
142
manager/app/src/main/cpp/ksu.c
Normal file
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// Created by weishu on 2022/12/9.
|
||||
//
|
||||
|
||||
#include <sys/prctl.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "prelude.h"
|
||||
#include "ksu.h"
|
||||
|
||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||
|
||||
#define CMD_GRANT_ROOT 0
|
||||
|
||||
#define CMD_BECOME_MANAGER 1
|
||||
#define CMD_GET_VERSION 2
|
||||
#define CMD_ALLOW_SU 3
|
||||
#define CMD_DENY_SU 4
|
||||
#define CMD_GET_SU_LIST 5
|
||||
#define CMD_GET_DENY_LIST 6
|
||||
#define CMD_CHECK_SAFEMODE 9
|
||||
|
||||
#define CMD_GET_APP_PROFILE 10
|
||||
#define CMD_SET_APP_PROFILE 11
|
||||
|
||||
#define CMD_IS_UID_GRANTED_ROOT 12
|
||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||
#define CMD_IS_SU_ENABLED 14
|
||||
#define CMD_ENABLE_SU 15
|
||||
|
||||
#define CMD_GET_VERSION_FULL 30
|
||||
|
||||
#define CMD_ENABLE_KPM 100
|
||||
#define CMD_HOOK_TYPE 101
|
||||
#define CMD_GET_SUSFS_FEATURE_STATUS 102
|
||||
|
||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||
int32_t result = 0;
|
||||
int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
|
||||
|
||||
return result == KERNEL_SU_OPTION && rtn == -1;
|
||||
}
|
||||
|
||||
bool become_manager(const char* pkg) {
|
||||
char param[128];
|
||||
uid_t uid = getuid();
|
||||
uint32_t userId = uid / 100000;
|
||||
if (userId == 0) {
|
||||
sprintf(param, "/data/data/%s", pkg);
|
||||
} else {
|
||||
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
|
||||
}
|
||||
|
||||
return ksuctl(CMD_BECOME_MANAGER, param, NULL);
|
||||
}
|
||||
|
||||
// cache the result to avoid unnecessary syscall
|
||||
static bool is_lkm;
|
||||
int get_version() {
|
||||
int32_t version = -1;
|
||||
int32_t flags = 0;
|
||||
ksuctl(CMD_GET_VERSION, &version, &flags);
|
||||
if (!is_lkm && (flags & 0x1)) {
|
||||
is_lkm = true;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
void get_full_version(char* buff) {
|
||||
ksuctl(CMD_GET_VERSION_FULL, buff, NULL);
|
||||
}
|
||||
|
||||
bool get_allow_list(int *uids, int *size) {
|
||||
return ksuctl(CMD_GET_SU_LIST, uids, size);
|
||||
}
|
||||
|
||||
bool is_safe_mode() {
|
||||
return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL);
|
||||
}
|
||||
|
||||
bool is_lkm_mode() {
|
||||
// you should call get_version first!
|
||||
return is_lkm;
|
||||
}
|
||||
|
||||
bool uid_should_umount(int uid) {
|
||||
int should;
|
||||
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should;
|
||||
}
|
||||
|
||||
bool set_app_profile(const struct app_profile* profile) {
|
||||
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL);
|
||||
}
|
||||
|
||||
bool get_app_profile(char* key, struct app_profile* profile) {
|
||||
return ksuctl(CMD_GET_APP_PROFILE, profile, NULL);
|
||||
}
|
||||
|
||||
bool set_su_enabled(bool enabled) {
|
||||
return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL);
|
||||
}
|
||||
|
||||
bool is_su_enabled() {
|
||||
int enabled = true;
|
||||
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
|
||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool is_KPM_enable() {
|
||||
int enabled = false;
|
||||
ksuctl(CMD_ENABLE_KPM, &enabled, NULL);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool get_hook_type(char* hook_type, size_t size) {
|
||||
if (hook_type == NULL || size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static char cached_hook_type[16] = {0};
|
||||
if (cached_hook_type[0] == '\0') {
|
||||
if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) {
|
||||
strcpy(cached_hook_type, "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
strncpy(hook_type, cached_hook_type, size);
|
||||
hook_type[size - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_susfs_feature_status(struct susfs_feature_status* status) {
|
||||
if (status == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//
|
||||
// Created by weishu on 2022/12/9.
|
||||
//
|
||||
|
||||
#include <sys/prctl.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ksu.h"
|
||||
|
||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||
|
||||
#define CMD_GRANT_ROOT 0
|
||||
|
||||
#define CMD_BECOME_MANAGER 1
|
||||
#define CMD_GET_VERSION 2
|
||||
#define CMD_ALLOW_SU 3
|
||||
#define CMD_DENY_SU 4
|
||||
#define CMD_GET_SU_LIST 5
|
||||
#define CMD_GET_DENY_LIST 6
|
||||
#define CMD_CHECK_SAFEMODE 9
|
||||
|
||||
#define CMD_GET_APP_PROFILE 10
|
||||
#define CMD_SET_APP_PROFILE 11
|
||||
|
||||
#define CMD_IS_UID_GRANTED_ROOT 12
|
||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||
#define CMD_IS_SU_ENABLED 14
|
||||
#define CMD_ENABLE_SU 15
|
||||
|
||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||
int32_t result = 0;
|
||||
prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
|
||||
return result == KERNEL_SU_OPTION;
|
||||
}
|
||||
|
||||
bool become_manager(const char* pkg) {
|
||||
char param[128];
|
||||
uid_t uid = getuid();
|
||||
uint32_t userId = uid / 100000;
|
||||
if (userId == 0) {
|
||||
sprintf(param, "/data/data/%s", pkg);
|
||||
} else {
|
||||
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
|
||||
}
|
||||
|
||||
return ksuctl(CMD_BECOME_MANAGER, param, nullptr);
|
||||
}
|
||||
|
||||
// cache the result to avoid unnecessary syscall
|
||||
static bool is_lkm;
|
||||
int get_version() {
|
||||
int32_t version = -1;
|
||||
int32_t lkm = 0;
|
||||
ksuctl(CMD_GET_VERSION, &version, &lkm);
|
||||
if (!is_lkm && lkm != 0) {
|
||||
is_lkm = true;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
bool get_allow_list(int *uids, int *size) {
|
||||
return ksuctl(CMD_GET_SU_LIST, uids, size);
|
||||
}
|
||||
|
||||
bool is_safe_mode() {
|
||||
return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr);
|
||||
}
|
||||
|
||||
bool is_lkm_mode() {
|
||||
// you should call get_version first!
|
||||
return is_lkm;
|
||||
}
|
||||
|
||||
bool uid_should_umount(int uid) {
|
||||
bool should;
|
||||
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast<void*>(uid), &should) && should;
|
||||
}
|
||||
|
||||
bool set_app_profile(const app_profile *profile) {
|
||||
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr);
|
||||
}
|
||||
|
||||
bool get_app_profile(p_key_t key, app_profile *profile) {
|
||||
return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr);
|
||||
}
|
||||
|
||||
bool set_su_enabled(bool enabled) {
|
||||
return ksuctl(CMD_ENABLE_SU, (void*) enabled, nullptr);
|
||||
}
|
||||
|
||||
bool is_su_enabled() {
|
||||
bool enabled = true;
|
||||
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
|
||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
||||
return enabled;
|
||||
}
|
||||
@@ -5,10 +5,13 @@
|
||||
#ifndef KERNELSU_KSU_H
|
||||
#define KERNELSU_KSU_H
|
||||
|
||||
#include "prelude.h"
|
||||
#include <linux/capability.h>
|
||||
|
||||
bool become_manager(const char *);
|
||||
|
||||
void get_full_version(char* buff);
|
||||
|
||||
int get_version();
|
||||
|
||||
bool get_allow_list(int *uids, int *size);
|
||||
@@ -25,7 +28,24 @@ bool is_lkm_mode();
|
||||
#define KSU_MAX_GROUPS 32
|
||||
#define KSU_SELINUX_DOMAIN 64
|
||||
|
||||
using p_key_t = char[KSU_MAX_PACKAGE_NAME];
|
||||
// SUSFS Functional State Structures
|
||||
struct susfs_feature_status {
|
||||
bool status_sus_path;
|
||||
bool status_sus_mount;
|
||||
bool status_auto_default_mount;
|
||||
bool status_auto_bind_mount;
|
||||
bool status_sus_kstat;
|
||||
bool status_try_umount;
|
||||
bool status_auto_try_umount_bind;
|
||||
bool status_spoof_uname;
|
||||
bool status_enable_log;
|
||||
bool status_hide_symbols;
|
||||
bool status_spoof_cmdline;
|
||||
bool status_open_redirect;
|
||||
bool status_magic_mount;
|
||||
bool status_overlayfs_auto_kstat;
|
||||
bool status_sus_su;
|
||||
};
|
||||
|
||||
struct root_profile {
|
||||
int32_t uid;
|
||||
@@ -75,12 +95,18 @@ struct app_profile {
|
||||
};
|
||||
};
|
||||
|
||||
bool set_app_profile(const app_profile *profile);
|
||||
bool set_app_profile(const struct app_profile* profile);
|
||||
|
||||
bool get_app_profile(p_key_t key, app_profile *profile);
|
||||
bool get_app_profile(char* key, struct app_profile* profile);
|
||||
|
||||
bool set_su_enabled(bool enabled);
|
||||
|
||||
bool is_su_enabled();
|
||||
|
||||
bool is_KPM_enable();
|
||||
|
||||
bool get_hook_type(char* hook_type, size_t size);
|
||||
|
||||
bool get_susfs_feature_status(struct susfs_feature_status* status);
|
||||
|
||||
#endif //KERNELSU_KSU_H
|
||||
17
manager/app/src/main/cpp/prelude.h
Normal file
17
manager/app/src/main/cpp/prelude.h
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
#ifndef KERNELSU_PRELUDE_H
|
||||
#define KERNELSU_PRELUDE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#define GetEnvironment() (*env)
|
||||
#define NativeBridge(fn, rtn, ...) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz, __VA_ARGS__)
|
||||
#define NativeBridgeNP(fn, rtn) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz)
|
||||
|
||||
#define LogDebug(...) __android_log_print(ANDROID_LOG_DEBUG, "KernelSU", __VA_ARGS__)
|
||||
|
||||
#endif
|
||||
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,20 +1,97 @@
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityOptions
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
class KernelSUApplication : Application() {
|
||||
private var currentActivity: Activity? = null
|
||||
|
||||
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
currentActivity = activity
|
||||
}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
if (currentActivity == activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
var context = base
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(base.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
context = base.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
override fun getResources(): Resources {
|
||||
val resources = super.getResources()
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return createConfigurationContext(config).resources
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ksuApp = this
|
||||
|
||||
// 注册Activity生命周期回调
|
||||
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||
|
||||
Platform.setHiddenApiExemptions()
|
||||
|
||||
val context = this
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
@@ -32,5 +109,43 @@ class KernelSUApplication : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
applyLanguageSetting()
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun applyLanguageSetting() {
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val resources = resources
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createConfigurationContext(config)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加刷新当前Activity的方法
|
||||
fun refreshCurrentActivity() {
|
||||
currentActivity?.let { activity ->
|
||||
val intent = activity.intent
|
||||
activity.finish()
|
||||
|
||||
val options = ActivityOptions.makeCustomAnimation(
|
||||
activity, android.R.anim.fade_in, android.R.anim.fade_out
|
||||
)
|
||||
activity.startActivity(intent, options.toBundle())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,13 @@ import android.system.Os
|
||||
*/
|
||||
|
||||
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||
override fun toString(): String {
|
||||
return "$major.$patchLevel.$subLevel"
|
||||
}
|
||||
|
||||
fun isGKI(): Boolean {
|
||||
|
||||
// kernel 6.x
|
||||
if (major > 5) {
|
||||
return true
|
||||
}
|
||||
|
||||
// kernel 5.10.x
|
||||
if (major == 5) {
|
||||
return patchLevel >= 10
|
||||
}
|
||||
|
||||
return false
|
||||
override fun toString(): String = "$major.$patchLevel.$subLevel"
|
||||
fun isGKI(): Boolean = when {
|
||||
major > 5 -> true
|
||||
major == 5 && patchLevel >= 10 -> true
|
||||
else -> false
|
||||
}
|
||||
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
|
||||
}
|
||||
|
||||
fun parseKernelVersion(version: String): KernelVersion {
|
||||
|
||||
@@ -16,19 +16,39 @@ 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 = 12800
|
||||
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5"
|
||||
|
||||
// 11640: Support query working mode, LKM or GKI
|
||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
|
||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||
|
||||
// 12040: Support disable sucompat mode
|
||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||
|
||||
const val MINIMAL_SUPPORTED_KPM = 12800
|
||||
|
||||
const val ROOT_UID = 0
|
||||
const val ROOT_GID = 0
|
||||
|
||||
external fun getFullVersion(): String
|
||||
|
||||
fun getSimpleVersionFull(): String {
|
||||
val fullVersion = getFullVersion()
|
||||
val startIndex = fullVersion.indexOf('v')
|
||||
if (startIndex < 0) {
|
||||
return fullVersion
|
||||
}
|
||||
val endIndex = fullVersion.indexOf('-', startIndex)
|
||||
val versionStr = if (endIndex > startIndex) {
|
||||
fullVersion.substring(startIndex, endIndex)
|
||||
} else {
|
||||
fullVersion.substring(startIndex)
|
||||
}
|
||||
return "v" + (Regex("""\d+(\.\d+)*""").find(versionStr)?.value ?: versionStr)
|
||||
}
|
||||
|
||||
init {
|
||||
System.loadLibrary("zako")
|
||||
}
|
||||
@@ -66,6 +86,14 @@ object Natives {
|
||||
*/
|
||||
external fun isSuEnabled(): Boolean
|
||||
external fun setSuEnabled(enabled: Boolean): Boolean
|
||||
external fun isKPMEnabled(): Boolean
|
||||
external fun getHookType(): String
|
||||
|
||||
/**
|
||||
* Get SUSFS feature status from kernel
|
||||
* @return SusfsFeatureStatus object containing all feature states, or null if failed
|
||||
*/
|
||||
external fun getSusfsFeatureStatus(): SusfsFeatureStatus?
|
||||
|
||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||
private const val NOBODY_UID = 9999
|
||||
@@ -88,9 +116,38 @@ object Natives {
|
||||
}
|
||||
|
||||
fun requireNewKernel(): Boolean {
|
||||
return version < MINIMAL_SUPPORTED_KERNEL
|
||||
if (version < MINIMAL_SUPPORTED_KERNEL) {
|
||||
return true
|
||||
}
|
||||
val simpleVersionFull = getSimpleVersionFull()
|
||||
if (simpleVersionFull.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return simpleVersionFull < MINIMAL_SUPPORTED_KERNEL_FULL
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class SusfsFeatureStatus(
|
||||
val statusSusPath: Boolean = false,
|
||||
val statusSusMount: Boolean = false,
|
||||
val statusAutoDefaultMount: Boolean = false,
|
||||
val statusAutoBindMount: Boolean = false,
|
||||
val statusSusKstat: Boolean = false,
|
||||
val statusTryUmount: Boolean = false,
|
||||
val statusAutoTryUmountBind: Boolean = false,
|
||||
val statusSpoofUname: Boolean = false,
|
||||
val statusEnableLog: Boolean = false,
|
||||
val statusHideSymbols: Boolean = false,
|
||||
val statusSpoofCmdline: Boolean = false,
|
||||
val statusOpenRedirect: Boolean = false,
|
||||
val statusMagicMount: Boolean = false,
|
||||
val statusOverlayfsAutoKstat: Boolean = false,
|
||||
val statusSusSu: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.sukisu.ultra.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.sukisu.zako.IKsuInterface;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/4/18.
|
||||
*/
|
||||
|
||||
public class KsuService extends RootService {
|
||||
|
||||
private static final String TAG = "KsuService";
|
||||
|
||||
class Stub extends IKsuInterface.Stub {
|
||||
@Override
|
||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
||||
Log.i(TAG, "getPackages: " + list.size());
|
||||
return new ParcelableListSlice<>(list);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
return new Stub();
|
||||
}
|
||||
|
||||
List<Integer> getUserIds() {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
|
||||
List<UserHandle> userProfiles = um.getUserProfiles();
|
||||
for (UserHandle userProfile : userProfiles) {
|
||||
int userId = userProfile.hashCode();
|
||||
result.add(userProfile.hashCode());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
|
||||
ArrayList<PackageInfo> packages = new ArrayList<>();
|
||||
for (Integer userId : getUserIds()) {
|
||||
Log.i(TAG, "getInstalledPackagesAll: " + userId);
|
||||
packages.addAll(getInstalledPackagesAsUser(flags, userId));
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
|
||||
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "err", e);
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -1,249 +1,213 @@
|
||||
package com.sukisu.ultra.ui
|
||||
|
||||
import android.database.ContentObserver
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
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.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
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.generated.destinations.ExecuteModuleActionScreenDestination
|
||||
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 zako.zako.zako.zakoui.activity.util.AppData
|
||||
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
|
||||
import zako.zako.zako.zakoui.activity.util.*
|
||||
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.install
|
||||
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.webui.initPlatform
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private inner class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
private lateinit var superUserViewModel: SuperUserViewModel
|
||||
private lateinit var homeViewModel: HomeViewModel
|
||||
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||
|
||||
data class SettingsState(
|
||||
val isHideOtherInfo: Boolean = false,
|
||||
val showKpmInfo: Boolean = false
|
||||
)
|
||||
|
||||
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||
|
||||
// 添加标记避免重复初始化
|
||||
private var isInitialized = false
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val context = LocaleUtils.applyLocale(newBase)
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
try {
|
||||
// 确保应用正确的语言设置
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
// 应用自定义 DPI
|
||||
DisplayUtils.applyCustomDpi(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
|
||||
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)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(applicationContext)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||
runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
// 使用标记控制初始化流程
|
||||
if (!isInitialized) {
|
||||
initializeViewModels()
|
||||
initializeData()
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||
|
||||
val showBottomBar = when (currentDestination?.route) {
|
||||
ExecuteModuleActionScreenDestination.route -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
initPlatform()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState
|
||||
) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||
showBottomBar = showBottomBar,
|
||||
content = { BottomBar(navController) }
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||
navController = navController,
|
||||
defaultTransitions = NavigationUtils.defaultTransitions()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeViewModels() {
|
||||
superUserViewModel = SuperUserViewModel()
|
||||
homeViewModel = HomeViewModel()
|
||||
|
||||
// 设置主题变化监听器
|
||||
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
|
||||
}
|
||||
|
||||
private fun initializeData() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
superUserViewModel.fetchAppList()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
val destroyListeners = mutableListOf<() -> Unit>()
|
||||
destroyListeners.add {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
homeViewModel.initializeData()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
// 数据刷新协程
|
||||
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
|
||||
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
|
||||
|
||||
// 初始化主题相关设置
|
||||
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
|
||||
|
||||
val isManager = AppData.isManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
override fun onResume() {
|
||||
try {
|
||||
super.onResume()
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
ThemeUtils.onActivityResume()
|
||||
|
||||
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)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// 仅在需要时刷新数据
|
||||
if (isInitialized) {
|
||||
refreshData()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshData() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
superUserViewModel.fetchAppList()
|
||||
homeViewModel.initializeData()
|
||||
DataRefreshUtils.refreshData(lifecycleScope)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
try {
|
||||
super.onPause()
|
||||
ThemeUtils.onActivityPause(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyListeners.forEach { it() }
|
||||
super.onDestroy()
|
||||
try {
|
||||
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
||||
super.onDestroy()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
try {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleUtils.applyLanguageSetting(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@SuppressLint("AutoboxingStateCreation")
|
||||
@Composable
|
||||
fun rememberFabVisibilityState(listState: LazyListState): State<Boolean> {
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
val fabVisible = remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
.collect { (index, offset) ->
|
||||
if (previousIndex == 0 && previousScrollOffset == 0) {
|
||||
fabVisible.value = true
|
||||
} else {
|
||||
val isScrollingDown = when {
|
||||
index > previousIndex -> false
|
||||
index < previousIndex -> true
|
||||
else -> offset < previousScrollOffset
|
||||
}
|
||||
|
||||
fabVisible.value = isScrollingDown
|
||||
}
|
||||
|
||||
previousIndex = index
|
||||
previousScrollOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
return fabVisible
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimatedFab(
|
||||
visible: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + scaleIn(),
|
||||
exit = fadeOut() + scaleOut(targetScale = 0.8f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.scale(scale)
|
||||
.alpha(scale)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -138,7 +137,7 @@ fun ImageEditorDialog(
|
||||
0f
|
||||
}
|
||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||
}
|
||||
}
|
||||
@@ -186,7 +185,7 @@ fun ImageEditorDialog(
|
||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||
savedUri?.let { onConfirm(it) }
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ fun SearchAppBar(
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
if (onSearch) {
|
||||
|
||||
@@ -5,14 +5,20 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
@@ -21,40 +27,71 @@ fun SwitchItem(
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
beta: Boolean = false,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.toggleable(
|
||||
value = checked,
|
||||
interactionSource = interactionSource,
|
||||
role = Role.Switch,
|
||||
enabled = enabled,
|
||||
indication = LocalIndication.current,
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
headlineContent = {
|
||||
Text(title)
|
||||
},
|
||||
leadingContent = icon?.let {
|
||||
{ Icon(icon, title) }
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(summary)
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.toggleable(
|
||||
value = checked,
|
||||
interactionSource = interactionSource,
|
||||
role = Role.Switch,
|
||||
enabled = enabled,
|
||||
indication = LocalIndication.current,
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
headlineContent = {
|
||||
TextRow(
|
||||
leadingContent = if (beta) {
|
||||
{
|
||||
LabelItem(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = "Beta"
|
||||
)
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = title,
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = icon?.let {
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
imageVector = icon,
|
||||
contentDescription = title
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = summary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
@@ -10,12 +9,10 @@ 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
|
||||
@@ -32,13 +29,19 @@ fun SlotSelectionDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSlotSelected: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
currentSlot = getCurrentSlot(context)
|
||||
currentSlot = getCurrentSlot()
|
||||
// 设置默认选择为当前槽位
|
||||
selectedSlot = when (currentSlot) {
|
||||
"a" -> "a"
|
||||
"b" -> "b"
|
||||
else -> null
|
||||
}
|
||||
errorMessage = null
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
@@ -47,11 +50,7 @@ fun SlotSelectionDialog(
|
||||
}
|
||||
|
||||
if (show) {
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -107,12 +106,12 @@ fun SlotSelectionDialog(
|
||||
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,
|
||||
subtitleText = 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,
|
||||
subtitleText = null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
)
|
||||
)
|
||||
@@ -128,19 +127,20 @@ fun SlotSelectionDialog(
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
color = if (option.subtitleText != null) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
onSlotSelected(
|
||||
when (index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
)
|
||||
selectedSlot = when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -148,7 +148,10 @@ fun SlotSelectionDialog(
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = if (option.subtitleText != null) {
|
||||
tint = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
@@ -163,7 +166,10 @@ fun SlotSelectionDialog(
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (option.subtitleText != null) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
@@ -173,7 +179,10 @@ fun SlotSelectionDialog(
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (true) {
|
||||
color = if (selectedSlot == when(index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -190,9 +199,10 @@ fun SlotSelectionDialog(
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
selectedSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
enabled = selectedSlot != null
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
@@ -225,7 +235,7 @@ data class ListOption(
|
||||
)
|
||||
|
||||
// Utility function to get current slot
|
||||
private fun getCurrentSlot(context: Context): String? {
|
||||
private fun getCurrentSlot(): String? {
|
||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||
if (it.startsWith("_")) it.substring(1) else it
|
||||
}
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
/**
|
||||
* 添加路径对话框
|
||||
*/
|
||||
@Composable
|
||||
fun AddPathDialog(
|
||||
showDialog: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String) -> Unit,
|
||||
isLoading: Boolean,
|
||||
titleRes: Int,
|
||||
labelRes: Int,
|
||||
placeholderRes: Int,
|
||||
initialValue: String = ""
|
||||
) {
|
||||
var newPath by remember { mutableStateOf("") }
|
||||
|
||||
// 当对话框显示时,设置初始值
|
||||
LaunchedEffect(showDialog, initialValue) {
|
||||
if (showDialog) {
|
||||
newPath = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(titleRes),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = newPath,
|
||||
onValueChange = { newPath = it },
|
||||
label = { Text(stringResource(labelRes)) },
|
||||
placeholder = { Text(stringResource(placeholderRes)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (newPath.isNotBlank()) {
|
||||
onConfirm(newPath.trim())
|
||||
newPath = ""
|
||||
}
|
||||
},
|
||||
enabled = newPath.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(if (initialValue.isNotEmpty()) R.string.susfs_save else R.string.add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
newPath = ""
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加尝试卸载对话框
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AddTryUmountDialog(
|
||||
showDialog: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, Int) -> Unit,
|
||||
isLoading: Boolean,
|
||||
initialPath: String = "",
|
||||
initialMode: Int = 0
|
||||
) {
|
||||
var newUmountPath by remember { mutableStateOf("") }
|
||||
var newUmountMode by remember { mutableIntStateOf(0) }
|
||||
var umountModeExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 当对话框显示时,设置初始值
|
||||
LaunchedEffect(showDialog, initialPath, initialMode) {
|
||||
if (showDialog) {
|
||||
newUmountPath = initialPath
|
||||
newUmountMode = initialMode
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(if (initialPath.isNotEmpty()) R.string.susfs_edit_try_umount else R.string.susfs_add_try_umount),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newUmountPath,
|
||||
onValueChange = { newUmountPath = it },
|
||||
label = { Text(stringResource(R.string.susfs_path_label)) },
|
||||
placeholder = { Text(stringResource(R.string.susfs_path_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = umountModeExpanded,
|
||||
onExpandedChange = { umountModeExpanded = !umountModeExpanded }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = if (newUmountMode == 0)
|
||||
stringResource(R.string.susfs_umount_mode_normal)
|
||||
else
|
||||
stringResource(R.string.susfs_umount_mode_detach),
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.susfs_umount_mode_label)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryEditable, true),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = umountModeExpanded,
|
||||
onDismissRequest = { umountModeExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.susfs_umount_mode_normal)) },
|
||||
onClick = {
|
||||
newUmountMode = 0
|
||||
umountModeExpanded = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.susfs_umount_mode_detach)) },
|
||||
onClick = {
|
||||
newUmountMode = 1
|
||||
umountModeExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (newUmountPath.isNotBlank()) {
|
||||
onConfirm(newUmountPath.trim(), newUmountMode)
|
||||
newUmountPath = ""
|
||||
newUmountMode = 0
|
||||
}
|
||||
},
|
||||
enabled = newUmountPath.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(if (initialPath.isNotEmpty()) R.string.susfs_save else R.string.add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
newUmountPath = ""
|
||||
newUmountMode = 0
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加Kstat静态配置对话框
|
||||
*/
|
||||
@Composable
|
||||
fun AddKstatStaticallyDialog(
|
||||
showDialog: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (String, String, String, String, String, String, String, String, String, String, String, String, String) -> Unit,
|
||||
isLoading: Boolean,
|
||||
initialConfig: String = ""
|
||||
) {
|
||||
var newKstatPath by remember { mutableStateOf("") }
|
||||
var newKstatIno by remember { mutableStateOf("") }
|
||||
var newKstatDev by remember { mutableStateOf("") }
|
||||
var newKstatNlink by remember { mutableStateOf("") }
|
||||
var newKstatSize by remember { mutableStateOf("") }
|
||||
var newKstatAtime by remember { mutableStateOf("") }
|
||||
var newKstatAtimeNsec by remember { mutableStateOf("") }
|
||||
var newKstatMtime by remember { mutableStateOf("") }
|
||||
var newKstatMtimeNsec by remember { mutableStateOf("") }
|
||||
var newKstatCtime by remember { mutableStateOf("") }
|
||||
var newKstatCtimeNsec by remember { mutableStateOf("") }
|
||||
var newKstatBlocks by remember { mutableStateOf("") }
|
||||
var newKstatBlksize by remember { mutableStateOf("") }
|
||||
|
||||
// 当对话框显示时,解析初始配置
|
||||
LaunchedEffect(showDialog, initialConfig) {
|
||||
if (showDialog && initialConfig.isNotEmpty()) {
|
||||
val parts = initialConfig.split("|")
|
||||
if (parts.size >= 13) {
|
||||
newKstatPath = parts[0]
|
||||
newKstatIno = if (parts[1] == "default") "" else parts[1]
|
||||
newKstatDev = if (parts[2] == "default") "" else parts[2]
|
||||
newKstatNlink = if (parts[3] == "default") "" else parts[3]
|
||||
newKstatSize = if (parts[4] == "default") "" else parts[4]
|
||||
newKstatAtime = if (parts[5] == "default") "" else parts[5]
|
||||
newKstatAtimeNsec = if (parts[6] == "default") "" else parts[6]
|
||||
newKstatMtime = if (parts[7] == "default") "" else parts[7]
|
||||
newKstatMtimeNsec = if (parts[8] == "default") "" else parts[8]
|
||||
newKstatCtime = if (parts[9] == "default") "" else parts[9]
|
||||
newKstatCtimeNsec = if (parts[10] == "default") "" else parts[10]
|
||||
newKstatBlocks = if (parts[11] == "default") "" else parts[11]
|
||||
newKstatBlksize = if (parts[12] == "default") "" else parts[12]
|
||||
}
|
||||
} else if (showDialog && initialConfig.isEmpty()) {
|
||||
// 清空所有字段
|
||||
newKstatPath = ""
|
||||
newKstatIno = ""
|
||||
newKstatDev = ""
|
||||
newKstatNlink = ""
|
||||
newKstatSize = ""
|
||||
newKstatAtime = ""
|
||||
newKstatAtimeNsec = ""
|
||||
newKstatMtime = ""
|
||||
newKstatMtimeNsec = ""
|
||||
newKstatCtime = ""
|
||||
newKstatCtimeNsec = ""
|
||||
newKstatBlocks = ""
|
||||
newKstatBlksize = ""
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
stringResource(if (initialConfig.isNotEmpty()) R.string.edit_kstat_statically_title else R.string.add_kstat_statically_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatPath,
|
||||
onValueChange = { newKstatPath = it },
|
||||
label = { Text(stringResource(R.string.file_or_directory_path_label)) },
|
||||
placeholder = { Text("/path/to/file_or_directory") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatIno,
|
||||
onValueChange = { newKstatIno = it },
|
||||
label = { Text("ino") },
|
||||
placeholder = { Text("1234") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatDev,
|
||||
onValueChange = { newKstatDev = it },
|
||||
label = { Text("dev") },
|
||||
placeholder = { Text("1234") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatNlink,
|
||||
onValueChange = { newKstatNlink = it },
|
||||
label = { Text("nlink") },
|
||||
placeholder = { Text("2") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatSize,
|
||||
onValueChange = { newKstatSize = it },
|
||||
label = { Text("size") },
|
||||
placeholder = { Text("223344") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatAtime,
|
||||
onValueChange = { newKstatAtime = it },
|
||||
label = { Text("atime") },
|
||||
placeholder = { Text("1712592355") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatAtimeNsec,
|
||||
onValueChange = { newKstatAtimeNsec = it },
|
||||
label = { Text("atime_nsec") },
|
||||
placeholder = { Text("0") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatMtime,
|
||||
onValueChange = { newKstatMtime = it },
|
||||
label = { Text("mtime") },
|
||||
placeholder = { Text("1712592355") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatMtimeNsec,
|
||||
onValueChange = { newKstatMtimeNsec = it },
|
||||
label = { Text("mtime_nsec") },
|
||||
placeholder = { Text("0") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatCtime,
|
||||
onValueChange = { newKstatCtime = it },
|
||||
label = { Text("ctime") },
|
||||
placeholder = { Text("1712592355") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatCtimeNsec,
|
||||
onValueChange = { newKstatCtimeNsec = it },
|
||||
label = { Text("ctime_nsec") },
|
||||
placeholder = { Text("0") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = newKstatBlocks,
|
||||
onValueChange = { newKstatBlocks = it },
|
||||
label = { Text("blocks") },
|
||||
placeholder = { Text("16") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = newKstatBlksize,
|
||||
onValueChange = { newKstatBlksize = it },
|
||||
label = { Text("blksize") },
|
||||
placeholder = { Text("512") },
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.hint_use_default_value),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (newKstatPath.isNotBlank()) {
|
||||
onConfirm(
|
||||
newKstatPath.trim(),
|
||||
newKstatIno.trim().ifBlank { "default" },
|
||||
newKstatDev.trim().ifBlank { "default" },
|
||||
newKstatNlink.trim().ifBlank { "default" },
|
||||
newKstatSize.trim().ifBlank { "default" },
|
||||
newKstatAtime.trim().ifBlank { "default" },
|
||||
newKstatAtimeNsec.trim().ifBlank { "default" },
|
||||
newKstatMtime.trim().ifBlank { "default" },
|
||||
newKstatMtimeNsec.trim().ifBlank { "default" },
|
||||
newKstatCtime.trim().ifBlank { "default" },
|
||||
newKstatCtimeNsec.trim().ifBlank { "default" },
|
||||
newKstatBlocks.trim().ifBlank { "default" },
|
||||
newKstatBlksize.trim().ifBlank { "default" }
|
||||
)
|
||||
// 清空所有字段
|
||||
newKstatPath = ""
|
||||
newKstatIno = ""
|
||||
newKstatDev = ""
|
||||
newKstatNlink = ""
|
||||
newKstatSize = ""
|
||||
newKstatAtime = ""
|
||||
newKstatAtimeNsec = ""
|
||||
newKstatMtime = ""
|
||||
newKstatMtimeNsec = ""
|
||||
newKstatCtime = ""
|
||||
newKstatCtimeNsec = ""
|
||||
newKstatBlocks = ""
|
||||
newKstatBlksize = ""
|
||||
}
|
||||
},
|
||||
enabled = newKstatPath.isNotBlank() && !isLoading,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(if (initialConfig.isNotEmpty()) R.string.susfs_save else R.string.add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismiss()
|
||||
// 清空所有字段
|
||||
newKstatPath = ""
|
||||
newKstatIno = ""
|
||||
newKstatDev = ""
|
||||
newKstatNlink = ""
|
||||
newKstatSize = ""
|
||||
newKstatAtime = ""
|
||||
newKstatAtimeNsec = ""
|
||||
newKstatMtime = ""
|
||||
newKstatMtimeNsec = ""
|
||||
newKstatCtime = ""
|
||||
newKstatCtimeNsec = ""
|
||||
newKstatBlocks = ""
|
||||
newKstatBlksize = ""
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框
|
||||
*/
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
showDialog: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
titleRes: Int,
|
||||
messageRes: Int,
|
||||
isLoading: Boolean = false,
|
||||
isDestructive: Boolean = false
|
||||
) {
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(messageRes)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
enabled = !isLoading,
|
||||
colors = if (isDestructive) {
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.buttonColors()
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
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.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.PathItemCard
|
||||
import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion_1_5_8
|
||||
|
||||
/**
|
||||
* SUS路径内容组件
|
||||
*/
|
||||
@Composable
|
||||
fun SusPathsContent(
|
||||
susPaths: Set<String>,
|
||||
isLoading: Boolean,
|
||||
onAddPath: () -> Unit,
|
||||
onRemovePath: (String) -> Unit,
|
||||
onEditPath: ((String) -> Unit)? = null
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (susPaths.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_paths_configured)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(susPaths.toList()) { path ->
|
||||
PathItemCard(
|
||||
path = path,
|
||||
icon = Icons.Default.Folder,
|
||||
onDelete = { onRemovePath(path) },
|
||||
onEdit = if (onEditPath != null) { { onEditPath(path) } } else null,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onAddPath,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SUS挂载内容组件
|
||||
*/
|
||||
@Composable
|
||||
fun SusMountsContent(
|
||||
susMounts: Set<String>,
|
||||
hideSusMountsForAllProcs: Boolean,
|
||||
isSusVersion_1_5_8: Boolean,
|
||||
isLoading: Boolean,
|
||||
onAddMount: () -> Unit,
|
||||
onRemoveMount: (String) -> Unit,
|
||||
onEditMount: ((String) -> Unit)? = null,
|
||||
onToggleHideSusMountsForAllProcs: (Boolean) -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (isSusVersion_1_5_8) {
|
||||
item {
|
||||
SusMountHidingControlCard(
|
||||
hideSusMountsForAllProcs = hideSusMountsForAllProcs,
|
||||
isLoading = isLoading,
|
||||
onToggleHiding = onToggleHideSusMountsForAllProcs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (susMounts.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_mounts_configured)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(susMounts.toList()) { mount ->
|
||||
PathItemCard(
|
||||
path = mount,
|
||||
icon = Icons.Default.Storage,
|
||||
onDelete = { onRemoveMount(mount) },
|
||||
onEdit = if (onEditMount != null) { { onEditMount(mount) } } else null,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onAddMount,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试卸载内容组件
|
||||
*/
|
||||
@Composable
|
||||
fun TryUmountContent(
|
||||
tryUmounts: Set<String>,
|
||||
umountForZygoteIsoService: Boolean,
|
||||
isLoading: Boolean,
|
||||
onAddUmount: () -> Unit,
|
||||
onRunUmount: () -> Unit,
|
||||
onRemoveUmount: (String) -> Unit,
|
||||
onEditUmount: ((String) -> Unit)? = null,
|
||||
onToggleUmountForZygoteIsoService: (Boolean) -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (isSusVersion_1_5_8()) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Security,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.umount_zygote_iso_service),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.umount_zygote_iso_service_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = umountForZygoteIsoService,
|
||||
onCheckedChange = onToggleUmountForZygoteIsoService,
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tryUmounts.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_umounts_configured)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(tryUmounts.toList()) { umountEntry ->
|
||||
val parts = umountEntry.split("|")
|
||||
val path = if (parts.isNotEmpty()) parts[0] else umountEntry
|
||||
val mode = if (parts.size > 1) parts[1] else "0"
|
||||
val modeText = if (mode == "0")
|
||||
stringResource(R.string.susfs_umount_mode_normal_short)
|
||||
else
|
||||
stringResource(R.string.susfs_umount_mode_detach_short)
|
||||
|
||||
PathItemCard(
|
||||
path = path,
|
||||
icon = Icons.Default.Storage,
|
||||
additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode),
|
||||
onDelete = { onRemoveUmount(umountEntry) },
|
||||
onEdit = if (onEditUmount != null) { { onEditUmount(umountEntry) } } else null,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onAddUmount,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
}
|
||||
|
||||
if (tryUmounts.isNotEmpty()) {
|
||||
Button(
|
||||
onClick = onRunUmount,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.susfs_run))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kstat配置内容组件
|
||||
*/
|
||||
@Composable
|
||||
fun KstatConfigContent(
|
||||
kstatConfigs: Set<String>,
|
||||
addKstatPaths: Set<String>,
|
||||
isLoading: Boolean,
|
||||
onAddKstatStatically: () -> Unit,
|
||||
onAddKstat: () -> Unit,
|
||||
onRemoveKstatConfig: (String) -> Unit,
|
||||
onEditKstatConfig: ((String) -> Unit)? = null,
|
||||
onRemoveAddKstat: (String) -> Unit,
|
||||
onEditAddKstat: ((String) -> Unit)? = null,
|
||||
onUpdateKstat: (String) -> Unit,
|
||||
onUpdateKstatFullClone: (String) -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 说明卡片
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_config_description_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_config_description_add_statically),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_config_description_add),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_config_description_update),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_config_description_update_full_clone),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 静态Kstat配置列表
|
||||
if (kstatConfigs.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.static_kstat_config),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
items(kstatConfigs.toList()) { config ->
|
||||
KstatConfigItemCard(
|
||||
config = config,
|
||||
onDelete = { onRemoveKstatConfig(config) },
|
||||
onEdit = if (onEditKstatConfig != null) { { onEditKstatConfig(config) } } else null,
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add Kstat路径列表
|
||||
if (addKstatPaths.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.kstat_path_management),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
items(addKstatPaths.toList()) { path ->
|
||||
AddKstatPathItemCard(
|
||||
path = path,
|
||||
onDelete = { onRemoveAddKstat(path) },
|
||||
onEdit = if (onEditAddKstat != null) { { onEditAddKstat(path) } } else null,
|
||||
onUpdate = { onUpdateKstat(path) },
|
||||
onUpdateFullClone = { onUpdateKstatFullClone(path) },
|
||||
isLoading = isLoading
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态显示
|
||||
if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.no_kstat_config_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加普通长按钮
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onAddKstat,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onAddKstatStatically,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = stringResource(R.string.add))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径设置内容组件
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
@Composable
|
||||
fun PathSettingsContent(
|
||||
androidDataPath: String,
|
||||
onAndroidDataPathChange: (String) -> Unit,
|
||||
sdcardPath: String,
|
||||
onSdcardPathChange: (String) -> Unit,
|
||||
isLoading: Boolean,
|
||||
onSetAndroidDataPath: () -> Unit,
|
||||
onSetSdcardPath: () -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Android Data路径设置
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = androidDataPath,
|
||||
onValueChange = onAndroidDataPathChange,
|
||||
label = { Text(stringResource(R.string.susfs_android_data_path_label)) },
|
||||
placeholder = { Text("/sdcard/Android/data") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onSetAndroidDataPath,
|
||||
enabled = !isLoading && androidDataPath.isNotBlank(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.susfs_set_android_data_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SD卡路径设置
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = sdcardPath,
|
||||
onValueChange = onSdcardPathChange,
|
||||
label = { Text(stringResource(R.string.susfs_sdcard_path_label)) },
|
||||
placeholder = { Text("/sdcard") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onSetSdcardPath,
|
||||
enabled = !isLoading && sdcardPath.isNotBlank(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.susfs_set_sdcard_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用功能状态内容组件
|
||||
*/
|
||||
@Composable
|
||||
fun EnabledFeaturesContent(
|
||||
enabledFeatures: List<SuSFSManager.EnabledFeature>,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 说明卡片
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_enabled_features_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledFeatures.isEmpty()) {
|
||||
item {
|
||||
EmptyStateCard(
|
||||
message = stringResource(R.string.susfs_no_features_found)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items(enabledFeatures) { feature ->
|
||||
FeatureStatusCard(
|
||||
feature = feature,
|
||||
onRefresh = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
// 菜单项数据类
|
||||
data class FabMenuItem(
|
||||
val icon: ImageVector,
|
||||
val labelRes: Int,
|
||||
val color: Color = Color.Unspecified,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
// 动画配置
|
||||
object FabAnimationConfig {
|
||||
const val ANIMATION_DURATION = 300
|
||||
const val STAGGER_DELAY = 50
|
||||
val BUTTON_SPACING = 72.dp
|
||||
val BUTTON_SIZE = 56.dp
|
||||
val SMALL_BUTTON_SIZE = 48.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalExpandableFab(
|
||||
menuItems: List<FabMenuItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonSize: Dp = FabAnimationConfig.BUTTON_SIZE,
|
||||
smallButtonSize: Dp = FabAnimationConfig.SMALL_BUTTON_SIZE,
|
||||
buttonSpacing: Dp = FabAnimationConfig.BUTTON_SPACING,
|
||||
animationDurationMs: Int = FabAnimationConfig.ANIMATION_DURATION,
|
||||
staggerDelayMs: Int = FabAnimationConfig.STAGGER_DELAY,
|
||||
mainButtonIcon: ImageVector = Icons.Filled.Add,
|
||||
mainButtonExpandedIcon: ImageVector = Icons.Filled.Close,
|
||||
onMainButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 主按钮旋转动画
|
||||
val rotationAngle by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 45f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "mainButtonRotation"
|
||||
)
|
||||
|
||||
// 主按钮缩放动画
|
||||
val mainButtonScale by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 1.1f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "mainButtonScale"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier.wrapContentSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
// 子菜单按钮
|
||||
menuItems.forEachIndexed { index, menuItem ->
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = if (isExpanded) {
|
||||
-(buttonSpacing.value * (index + 1))
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
delayMillis = if (isExpanded) {
|
||||
index * staggerDelayMs
|
||||
} else {
|
||||
(menuItems.size - index - 1) * staggerDelayMs
|
||||
},
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "fabOffset$index"
|
||||
)
|
||||
|
||||
val animatedScale by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 1f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
delayMillis = if (isExpanded) {
|
||||
index * staggerDelayMs + 100
|
||||
} else {
|
||||
(menuItems.size - index - 1) * staggerDelayMs
|
||||
},
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "fabScale$index"
|
||||
)
|
||||
|
||||
val animatedAlpha by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 1f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
delayMillis = if (isExpanded) {
|
||||
index * staggerDelayMs + 150
|
||||
} else {
|
||||
(menuItems.size - index - 1) * staggerDelayMs
|
||||
},
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "fabAlpha$index"
|
||||
)
|
||||
|
||||
// 子按钮容器(包含标签)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.offset(y = animatedOffsetY.dp)
|
||||
.scale(animatedScale)
|
||||
.alpha(animatedAlpha),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
// 标签
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded && animatedScale > 0.5f,
|
||||
enter = slideInHorizontally(
|
||||
initialOffsetX = { it / 2 },
|
||||
animationSpec = tween(200)
|
||||
) + fadeIn(animationSpec = tween(200)),
|
||||
exit = slideOutHorizontally(
|
||||
targetOffsetX = { it / 2 },
|
||||
animationSpec = tween(150)
|
||||
) + fadeOut(animationSpec = tween(150))
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
tonalElevation = 6.dp
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(menuItem.labelRes),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.inverseOnSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 子按钮
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
menuItem.onClick()
|
||||
isExpanded = false
|
||||
},
|
||||
modifier = Modifier.size(smallButtonSize),
|
||||
containerColor = if (menuItem.color != Color.Unspecified) {
|
||||
menuItem.color
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
},
|
||||
contentColor = if (menuItem.color != Color.Unspecified) {
|
||||
if (menuItem.color == Color.Gray) Color.White
|
||||
else MaterialTheme.colorScheme.onSecondary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSecondary
|
||||
},
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 4.dp,
|
||||
pressedElevation = 6.dp
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = menuItem.icon,
|
||||
contentDescription = stringResource(menuItem.labelRes),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主按钮
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
onMainButtonClick?.invoke()
|
||||
isExpanded = !isExpanded
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.scale(mainButtonScale),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 6.dp,
|
||||
pressedElevation = 8.dp,
|
||||
hoveredElevation = 8.dp
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isExpanded) mainButtonExpandedIcon else mainButtonIcon,
|
||||
contentDescription = stringResource(
|
||||
if (isExpanded) R.string.collapse_menu else R.string.expand_menu
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.rotate(if (mainButtonIcon == Icons.Filled.Add) rotationAngle else 0f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预设菜单项
|
||||
object FabMenuPresets {
|
||||
fun getScrollMenuItems(
|
||||
onScrollToTop: () -> Unit,
|
||||
onScrollToBottom: () -> Unit
|
||||
) = listOf(
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.KeyboardArrowDown,
|
||||
labelRes = R.string.scroll_to_bottom,
|
||||
onClick = onScrollToBottom
|
||||
),
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.KeyboardArrowUp,
|
||||
labelRes = R.string.scroll_to_top,
|
||||
onClick = onScrollToTop
|
||||
)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun getBatchActionMenuItems(
|
||||
onCancel: () -> Unit,
|
||||
onDeny: () -> Unit,
|
||||
onAllow: () -> Unit,
|
||||
onUnmountModules: () -> Unit,
|
||||
onDisableUnmount: () -> Unit
|
||||
) = listOf(
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.Close,
|
||||
labelRes = R.string.cancel,
|
||||
color = Color.Gray,
|
||||
onClick = onCancel
|
||||
),
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.Block,
|
||||
labelRes = R.string.deny_authorization,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
onClick = onDeny
|
||||
),
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.Check,
|
||||
labelRes = R.string.grant_authorization,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onAllow
|
||||
),
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.FolderOff,
|
||||
labelRes = R.string.unmount_modules,
|
||||
onClick = onUnmountModules
|
||||
),
|
||||
FabMenuItem(
|
||||
icon = Icons.Filled.Folder,
|
||||
labelRes = R.string.disable_unmount,
|
||||
onClick = onDisableUnmount
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,6 @@ fun AppProfileConfig(
|
||||
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
title = stringResource(R.string.profile_umount_modules),
|
||||
summary = stringResource(R.string.profile_umount_modules_summary),
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -47,6 +48,8 @@ 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.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -206,28 +209,35 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_groups),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options,
|
||||
maxChoices = 32, // Kernel only supports 32 groups at most
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = groups[index]
|
||||
selection.add(group)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_groups),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options,
|
||||
maxChoices = 32, // Kernel only supports 32 groups at most
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = groups[index]
|
||||
selection.add(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
@@ -278,27 +288,34 @@ fun CapsPanel(
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_capabilities),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = caps[index]
|
||||
selection.add(group)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_capabilities),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = caps[index]
|
||||
selection.add(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
@@ -425,24 +442,32 @@ private fun SELinuxPanel(
|
||||
)
|
||||
)
|
||||
|
||||
InputDialog(
|
||||
state = rememberUseCaseState(visible = true,
|
||||
onFinishedRequest = {
|
||||
onSELinuxChange(domain, rules)
|
||||
},
|
||||
onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_selinux_context),
|
||||
),
|
||||
selection = InputSelection(
|
||||
input = inputOptions,
|
||||
onPositiveClick = { result ->
|
||||
// Handle selection
|
||||
},
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
)
|
||||
) {
|
||||
InputDialog(
|
||||
state = rememberUseCaseState(
|
||||
visible = true,
|
||||
onFinishedRequest = {
|
||||
onSELinuxChange(domain, rules)
|
||||
},
|
||||
onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_selinux_context),
|
||||
),
|
||||
selection = InputSelection(
|
||||
input = inputOptions,
|
||||
onPositiveClick = { result ->
|
||||
// Handle selection
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ListItem(headlineContent = {
|
||||
|
||||
@@ -37,7 +37,6 @@ 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
|
||||
@@ -60,6 +59,7 @@ 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.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -84,6 +84,9 @@ 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.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.forceStopApp
|
||||
import com.sukisu.ultra.ui.util.getSepolicy
|
||||
@@ -122,7 +125,12 @@ fun AppProfileScreen(
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
@@ -203,149 +211,173 @@ private fun AppProfileInner(
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
val cardColors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
|
||||
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,
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
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,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
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
|
||||
)
|
||||
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,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
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 -> {}
|
||||
}
|
||||
Mode.Custom -> {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
} 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
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = mode == Mode.Custom,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
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,
|
||||
colors = cardColors,
|
||||
elevation = getCardElevation(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = mode == Mode.Custom,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,12 +409,10 @@ private fun TopBar(
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -391,9 +421,6 @@ private fun TopBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
@@ -408,7 +435,6 @@ private fun TopBar(
|
||||
modifier = Modifier.shadow(
|
||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||
4.dp else 0.dp,
|
||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -431,7 +457,6 @@ private fun ProfileBox(
|
||||
Text(
|
||||
text = mode.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
@@ -444,7 +469,6 @@ private fun ProfileBox(
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = Dp.Hairline,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
@@ -574,20 +598,26 @@ private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||
@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
|
||||
},
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
Surface {
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Android,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.sukisu.ultra.ui.screen
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||
@@ -21,8 +22,8 @@ enum class BottomBarDestination(
|
||||
val rootRequired: Boolean,
|
||||
) {
|
||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true),
|
||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, true),
|
||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -30,7 +37,6 @@ import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
var actionResult: Boolean
|
||||
var isActionRunning by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
BackHandler(enabled = isActionRunning) {
|
||||
// Disable back button if action is running
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
@@ -76,33 +86,43 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
onStderr = {
|
||||
logContent.append(it).append("\n")
|
||||
}
|
||||
).let {
|
||||
actionResult = it
|
||||
}
|
||||
)
|
||||
}
|
||||
if (actionResult) navigator.popBackStack()
|
||||
isActionRunning = false
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
},
|
||||
isActionRunning = isActionRunning,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"KernelSU_module_action_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
if (!isActionRunning) {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"KernelSU_module_action_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!isActionRunning) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(R.string.close)) },
|
||||
icon = { Icon(Icons.Filled.Close, contentDescription = null) },
|
||||
onClick = {
|
||||
navigator.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing,
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.action)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
IconButton(
|
||||
onClick = onSave,
|
||||
enabled = !isActionRunning
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.save_log),
|
||||
|
||||
@@ -3,27 +3,40 @@ package com.sukisu.ultra.ui.screen
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
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.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.Error
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -33,10 +46,17 @@ import kotlinx.parcelize.Parcelize
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
enum class FlashingStatus {
|
||||
FLASHING,
|
||||
SUCCESS,
|
||||
@@ -45,44 +65,139 @@ enum class FlashingStatus {
|
||||
|
||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||
|
||||
// 添加模块安装状态跟踪
|
||||
data class ModuleInstallStatus(
|
||||
val totalModules: Int = 0,
|
||||
val currentModule: Int = 0,
|
||||
val currentModuleName: String = "",
|
||||
val failedModules: MutableList<String> = mutableListOf()
|
||||
)
|
||||
|
||||
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
|
||||
|
||||
fun setFlashingStatus(status: FlashingStatus) {
|
||||
currentFlashingStatus.value = status
|
||||
}
|
||||
|
||||
fun updateModuleInstallStatus(
|
||||
totalModules: Int? = null,
|
||||
currentModule: Int? = null,
|
||||
currentModuleName: String? = null,
|
||||
failedModule: String? = null
|
||||
) {
|
||||
val current = moduleInstallStatus.value
|
||||
moduleInstallStatus.value = current.copy(
|
||||
totalModules = totalModules ?: current.totalModules,
|
||||
currentModule = currentModule ?: current.currentModule,
|
||||
currentModuleName = currentModuleName ?: current.currentModuleName
|
||||
)
|
||||
|
||||
if (failedModule != null) {
|
||||
val updatedFailedModules = current.failedModules.toMutableList()
|
||||
updatedFailedModules.add(failedModule)
|
||||
moduleInstallStatus.value = moduleInstallStatus.value.copy(
|
||||
failedModules = updatedFailedModules
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Destination<RootGraph>
|
||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
val context = LocalContext.current
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
var tempText: String
|
||||
val logContent = rememberSaveable { StringBuilder() }
|
||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||
// 添加状态跟踪是否已经完成刷写
|
||||
var hasFlashCompleted by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val viewModel: ModuleViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
val errorCodeString = stringResource(R.string.error_code)
|
||||
val checkLogString = stringResource(R.string.check_log)
|
||||
val logSavedString = stringResource(R.string.log_saved)
|
||||
val installingModuleString = stringResource(R.string.installing_module)
|
||||
|
||||
// 当前模块安装状态
|
||||
val currentStatus = moduleInstallStatus.value
|
||||
|
||||
// 重置状态
|
||||
LaunchedEffect(flashIt) {
|
||||
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex == 0) {
|
||||
moduleInstallStatus.value = ModuleInstallStatus(
|
||||
totalModules = flashIt.uris.size,
|
||||
currentModule = 1
|
||||
)
|
||||
hasFlashCompleted = false
|
||||
} else if (flashIt !is FlashIt.FlashModules) {
|
||||
hasFlashCompleted = false
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在未完成刷写时才执行刷写操作
|
||||
LaunchedEffect(flashIt, hasFlashCompleted) {
|
||||
// 如果已经完成刷写或者已有文本内容,则不再执行
|
||||
if (hasFlashCompleted || text.isNotEmpty()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
setFlashingStatus(FlashingStatus.FLASHING)
|
||||
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
try {
|
||||
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||
val moduleName = getModuleNameFromUri(context, currentUri)
|
||||
updateModuleInstallStatus(
|
||||
currentModuleName = moduleName
|
||||
)
|
||||
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName)
|
||||
logContent.append(text).append("\n")
|
||||
} catch (_: Exception) {
|
||||
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module")
|
||||
logContent.append(text).append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
flashIt(flashIt, onFinish = { showReboot, code ->
|
||||
if (code != 0) {
|
||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
||||
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||
setFlashingStatus(FlashingStatus.FAILED)
|
||||
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
updateModuleInstallStatus(
|
||||
failedModule = moduleInstallStatus.value.currentModuleName
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||
viewModel.markNeedRefresh()
|
||||
}
|
||||
if (showReboot) {
|
||||
text += "\n\n\n"
|
||||
showFloatAction = true
|
||||
}
|
||||
|
||||
hasFlashCompleted = true
|
||||
|
||||
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
|
||||
val nextFlashIt = flashIt.copy(
|
||||
currentIndex = flashIt.currentIndex + 1
|
||||
)
|
||||
scope.launch {
|
||||
kotlinx.coroutines.delay(500)
|
||||
navigator.navigate(FlashScreenDestination(nextFlashIt))
|
||||
}
|
||||
}
|
||||
}, onStdout = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
text = tempText.substring(6)
|
||||
} else {
|
||||
text += tempText
|
||||
@@ -94,13 +209,30 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
}
|
||||
}
|
||||
|
||||
val onBack: () -> Unit = {
|
||||
if (currentFlashingStatus.value != FlashingStatus.FLASHING) {
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
viewModel.markNeedRefresh()
|
||||
viewModel.fetchModuleList()
|
||||
navigator.navigate(ModuleScreenDestination)
|
||||
} else {
|
||||
viewModel.markNeedRefresh()
|
||||
viewModel.fetchModuleList()
|
||||
navigator.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
currentFlashingStatus.value,
|
||||
onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
},
|
||||
currentStatus,
|
||||
onBack = onBack,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
@@ -110,7 +242,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
"KernelSU_install_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
@@ -118,8 +250,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (showFloatAction) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
@@ -128,36 +258,271 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
||||
text = { Text(text = reboot) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(id = R.string.reboot)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.reboot))
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
expanded = true
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(scrollState),
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) {
|
||||
LaunchedEffect(text) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
if (flashIt is FlashIt.FlashModules) {
|
||||
ModuleInstallProgressBar(
|
||||
currentIndex = flashIt.currentIndex + 1,
|
||||
totalCount = flashIt.uris.size,
|
||||
currentModuleName = currentStatus.currentModuleName,
|
||||
status = currentFlashingStatus.value,
|
||||
failedModules = currentStatus.failedModules
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = text,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
LaunchedEffect(text) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示模块安装进度条和状态
|
||||
@Composable
|
||||
fun ModuleInstallProgressBar(
|
||||
currentIndex: Int,
|
||||
totalCount: Int,
|
||||
currentModuleName: String,
|
||||
status: FlashingStatus,
|
||||
failedModules: List<String>
|
||||
) {
|
||||
val progressColor = when(status) {
|
||||
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
val progress = animateFloatAsState(
|
||||
targetValue = currentIndex.toFloat() / totalCount.toFloat(),
|
||||
label = "InstallProgress"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 模块名称和进度
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = if (currentModuleName.isNotEmpty()) currentModuleName else stringResource(R.string.module),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$currentIndex/$totalCount",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 进度条
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.value },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
color = progressColor,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 失败模块列表
|
||||
AnimatedVisibility(
|
||||
visible = failedModules.isNotEmpty(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.module_failed_count, failedModules.size),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// 失败模块列表
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
failedModules.forEach { moduleName ->
|
||||
Text(
|
||||
text = "• $moduleName",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
status: FlashingStatus,
|
||||
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
|
||||
onBack: () -> Unit,
|
||||
onSave: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
val statusColor = when(status) {
|
||||
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
when (status) {
|
||||
FlashingStatus.FLASHING -> R.string.flashing
|
||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
||||
FlashingStatus.FAILED -> R.string.flash_failed
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = statusColor
|
||||
)
|
||||
|
||||
if (moduleStatus.failedModules.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.save_log),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (uri == Uri.EMPTY) {
|
||||
return@withContext context.getString(R.string.unknown_module)
|
||||
}
|
||||
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||
return@withContext context.getString(R.string.unknown_module)
|
||||
}
|
||||
ModuleUtils.extractModuleName(context, uri)
|
||||
} catch (_: Exception) {
|
||||
context.getString(R.string.unknown_module)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +531,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
sealed class FlashIt : Parcelable {
|
||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
||||
data class FlashModule(val uri: Uri) : FlashIt()
|
||||
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
|
||||
data object FlashRestore : FlashIt()
|
||||
data object FlashUninstall : FlashIt()
|
||||
}
|
||||
@@ -186,49 +552,22 @@ fun flashIt(
|
||||
onStderr
|
||||
)
|
||||
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
||||
is FlashIt.FlashModules -> {
|
||||
if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) {
|
||||
onFinish(false, 0)
|
||||
return
|
||||
}
|
||||
|
||||
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||
onStdout("\n")
|
||||
|
||||
flashModule(currentUri, onFinish, onStdout, onStderr)
|
||||
}
|
||||
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
||||
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
status: FlashingStatus,
|
||||
onBack: () -> Unit = {},
|
||||
onSave: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(
|
||||
when (status) {
|
||||
FlashingStatus.FLASHING -> R.string.flashing
|
||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
||||
FlashingStatus.FAILED -> R.string.flash_failed
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = "Localized description"
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun FlashScreenPreview() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@ 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
|
||||
@@ -51,7 +50,6 @@ 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
|
||||
@@ -61,6 +59,7 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -75,12 +74,10 @@ 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.generated.destinations.KernelFlashScreenDestination
|
||||
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
|
||||
@@ -95,10 +92,12 @@ 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
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@@ -110,16 +109,10 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
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
|
||||
}
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
@@ -145,14 +138,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(
|
||||
context = context,
|
||||
state = horizonKernelState,
|
||||
slot = method.slot
|
||||
navigator.navigate(
|
||||
KernelFlashScreenDestination(
|
||||
kernelUri = uri,
|
||||
selectedSlot = method.slot
|
||||
)
|
||||
)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -195,28 +186,13 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank() && installMethod !is InstallMethod.HorizonKernel) {
|
||||
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())
|
||||
|
||||
@@ -224,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
@@ -241,7 +216,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
SelectInstallMethod(
|
||||
isGKI = isGKI,
|
||||
isAbDevice = isAbDevice,
|
||||
onSelected = { method ->
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||
if (isAbDevice) {
|
||||
@@ -253,18 +227,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
} 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()
|
||||
@@ -273,7 +238,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
@@ -299,7 +264,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
if (method.slot != null) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
@@ -325,7 +290,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null && !flashState.isFlashing,
|
||||
enabled = installMethod != null,
|
||||
onClick = onClickNext,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
@@ -401,7 +366,6 @@ sealed class InstallMethod {
|
||||
@Composable
|
||||
private fun SelectInstallMethod(
|
||||
isGKI: Boolean = false,
|
||||
isAbDevice: Boolean = false,
|
||||
onSelected: (InstallMethod) -> Unit = {}
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
@@ -409,7 +373,11 @@ private fun SelectInstallMethod(
|
||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip,
|
||||
if (isInitBoot()) "init_boot" else "boot"
|
||||
if (isInitBoot()) {
|
||||
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
|
||||
} else {
|
||||
"boot"
|
||||
}
|
||||
)
|
||||
|
||||
val radioOptions = mutableListOf<InstallMethod>(
|
||||
@@ -488,8 +456,8 @@ private fun SelectInstallMethod(
|
||||
}
|
||||
}
|
||||
|
||||
var LKMExpanded by remember { mutableStateOf(false) }
|
||||
var GKIExpanded by remember { mutableStateOf(false) }
|
||||
var lkmExpanded by remember { mutableStateOf(false) }
|
||||
var gkiExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
@@ -498,38 +466,39 @@ private fun SelectInstallMethod(
|
||||
if (isGKI) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.padding(bottom = 16.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
|
||||
}
|
||||
)
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
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,
|
||||
visible = lkmExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
@@ -604,38 +573,39 @@ private fun SelectInstallMethod(
|
||||
if (rootAvailable) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
elevation = getCardElevation(),
|
||||
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
|
||||
}
|
||||
)
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
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,
|
||||
visible = gkiExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
@@ -647,60 +617,60 @@ private fun SelectInstallMethod(
|
||||
)
|
||||
) {
|
||||
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,
|
||||
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()
|
||||
.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
|
||||
)
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
.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 = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,11 +692,10 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
}
|
||||
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
@@ -749,10 +718,14 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
|
||||
@@ -5,9 +5,13 @@ import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
@@ -38,6 +42,7 @@ import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.*
|
||||
import android.app.Activity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
/**
|
||||
@@ -57,6 +62,9 @@ fun KpmScreen(
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val fabVisible by rememberFabVisibilityState(listState)
|
||||
|
||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||
val moduleFileName = module.id
|
||||
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
|
||||
@@ -112,7 +120,6 @@ fun KpmScreen(
|
||||
Text(
|
||||
text = kpmInstallMode,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
@@ -121,7 +128,6 @@ fun KpmScreen(
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -146,9 +152,6 @@ fun KpmScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Download,
|
||||
@@ -176,9 +179,6 @@ fun KpmScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inventory,
|
||||
@@ -209,7 +209,6 @@ fun KpmScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
@@ -292,38 +291,34 @@ fun KpmScreen(
|
||||
)
|
||||
},
|
||||
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,
|
||||
)
|
||||
AnimatedFab(visible = fabVisible) {
|
||||
FloatingActionButton(
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.package_import),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
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)
|
||||
@@ -348,7 +343,6 @@ fun KpmScreen(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
IconButton(
|
||||
@@ -357,9 +351,6 @@ fun KpmScreen(
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
},
|
||||
modifier = Modifier.size(24.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
@@ -391,12 +382,12 @@ fun KpmScreen(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
@@ -585,7 +576,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_control),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
@@ -595,20 +585,14 @@ private fun KpmModuleItem(
|
||||
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 = {
|
||||
@@ -627,7 +611,6 @@ private fun KpmModuleItem(
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -635,26 +618,16 @@ private fun KpmModuleItem(
|
||||
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)
|
||||
)
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
@@ -668,7 +641,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@@ -676,19 +648,16 @@ private fun KpmModuleItem(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -698,7 +667,6 @@ private fun KpmModuleItem(
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
@@ -711,10 +679,6 @@ private fun KpmModuleItem(
|
||||
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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,925 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@ import android.net.Uri
|
||||
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.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -27,6 +31,7 @@ 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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -48,17 +53,22 @@ 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
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
private val SPACING_SMALL = 3.dp
|
||||
private val SPACING_MEDIUM = 8.dp
|
||||
private val SPACING_LARGE = 16.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@@ -66,12 +76,18 @@ import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val context = LocalContext.current
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
var selectedEngine by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getString("webui_engine", "default") ?: "default"
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
// containerColor = MaterialTheme.colorScheme.surfaceBright,
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
TopBar(scrollBehavior = scrollBehavior)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
@@ -80,7 +96,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
// endregion
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -88,12 +103,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// region 上下文与协程
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
// endregion
|
||||
|
||||
// region 日志导出功能
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
@@ -110,31 +121,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置
|
||||
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)
|
||||
)
|
||||
|
||||
// 配置卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.configuration),
|
||||
content = {
|
||||
// 配置文件模板入口
|
||||
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),
|
||||
title = stringResource(R.string.settings_profile_template),
|
||||
summary = stringResource(R.string.settings_profile_template_summary),
|
||||
onClick = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
@@ -147,198 +143,176 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
title = stringResource(R.string.settings_umount_modules_default),
|
||||
summary = stringResource(R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked,
|
||||
onCheckedChange = {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
onCheckedChange = { enabled ->
|
||||
if (Natives.setDefaultUmountModules(enabled)) {
|
||||
umountChecked = enabled
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
// SU 禁用开关
|
||||
KsuIsValid {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
title = stringResource(R.string.settings_disable_su),
|
||||
summary = stringResource(R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
onCheckedChange = { checked ->
|
||||
val shouldEnable = !checked
|
||||
onCheckedChange = { enabled ->
|
||||
val shouldEnable = !enabled
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
isSuDisabled = enabled
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 应用设置
|
||||
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)
|
||||
)
|
||||
|
||||
// 应用设置卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.app_settings),
|
||||
content = {
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
mutableStateOf(prefs.getBoolean("check_update", true))
|
||||
}
|
||||
SwitchSettingItem(
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
title = stringResource(R.string.settings_check_update),
|
||||
summary = stringResource(R.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
onCheckedChange = { enabled ->
|
||||
prefs.edit { putBoolean("check_update", enabled) }
|
||||
checkUpdate = enabled
|
||||
}
|
||||
)
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
// WebUI引擎选择
|
||||
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
|
||||
WebUIEngineSelector(
|
||||
selectedEngine = selectedEngine,
|
||||
onEngineSelected = { engine ->
|
||||
selectedEngine = engine
|
||||
prefs.edit { putString("webui_engine", engine) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Web调试和Web X Eruda 开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(prefs.getBoolean("enable_web_debugging", false))
|
||||
}
|
||||
var useWebUIXEruda by rememberSaveable {
|
||||
mutableStateOf(prefs.getBoolean("use_webuix_eruda", false))
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(R.string.enable_web_debugging),
|
||||
summary = stringResource(R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = { enabled ->
|
||||
prefs.edit { putBoolean("enable_web_debugging", enabled) }
|
||||
enableWebDebugging = enabled
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = enableWebDebugging && selectedEngine == "wx",
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FormatListNumbered,
|
||||
title = stringResource(R.string.use_webuix_eruda),
|
||||
summary = stringResource(R.string.use_webuix_eruda_summary),
|
||||
checked = useWebUIXEruda,
|
||||
onCheckedChange = { enabled ->
|
||||
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
|
||||
useWebUIXEruda = enabled
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 更多设置
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Settings,
|
||||
title = stringResource(id = R.string.more_settings),
|
||||
summary = stringResource(id = R.string.more_settings),
|
||||
title = stringResource(R.string.more_settings),
|
||||
summary = stringResource(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)
|
||||
)
|
||||
)
|
||||
|
||||
// 工具卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.tools),
|
||||
content = {
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.BugReport,
|
||||
title = stringResource(id = R.string.send_log),
|
||||
title = stringResource(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
|
||||
LogBottomSheet(
|
||||
onDismiss = { showBottomsheet = false },
|
||||
onSaveLog = {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
},
|
||||
onShareLog = {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val 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
|
||||
@@ -348,26 +322,12 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 关于
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
// 关于卡片
|
||||
SettingsGroupCard(
|
||||
title = stringResource(R.string.about),
|
||||
content = {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Info,
|
||||
title = stringResource(R.string.about),
|
||||
@@ -376,13 +336,128 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsGroupCard(
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
elevation = getCardElevation()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = SPACING_MEDIUM)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WebUIEngineSelector(
|
||||
selectedEngine: String,
|
||||
onEngineSelected: (String) -> Unit
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val engineOptions = listOf(
|
||||
"default" to stringResource(R.string.engine_auto_select),
|
||||
"wx" to stringResource(R.string.engine_force_webuix),
|
||||
"ksu" to stringResource(R.string.engine_force_ksu)
|
||||
)
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.WebAsset,
|
||||
title = stringResource(R.string.use_webuix),
|
||||
summary = engineOptions.find { it.first == selectedEngine }?.second
|
||||
?: stringResource(R.string.engine_auto_select),
|
||||
onClick = { showDialog = true }
|
||||
)
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
title = { Text(stringResource(R.string.use_webuix)) },
|
||||
text = {
|
||||
Column {
|
||||
engineOptions.forEach { (value, label) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onEngineSelected(value)
|
||||
showDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selectedEngine == value,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDialog = false }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LogBottomSheet(
|
||||
onDismiss: () -> Unit,
|
||||
onSaveLog: () -> Unit,
|
||||
onShareLog: () -> Unit
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(SPACING_LARGE),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Save,
|
||||
text = stringResource(R.string.save_log),
|
||||
onClick = onSaveLog
|
||||
)
|
||||
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Share,
|
||||
text = stringResource(R.string.send_log),
|
||||
onClick = onShareLog
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogActionButton(
|
||||
icon: ImageVector,
|
||||
@@ -393,7 +468,7 @@ fun LogActionButton(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
.padding(SPACING_MEDIUM)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -409,11 +484,10 @@ fun LogActionButton(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -429,33 +503,31 @@ fun SettingItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.padding(end = SPACING_LARGE)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (summary != null) {
|
||||
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronRight,
|
||||
contentDescription = null,
|
||||
@@ -466,7 +538,7 @@ fun SettingItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchSettingItem(
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
@@ -477,44 +549,34 @@ fun SwitchSettingItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.padding(end = SPACING_LARGE)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (summary != null) {
|
||||
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -598,97 +660,123 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
|
||||
|
||||
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,
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_uninstall),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
options.forEachIndexed { index, option ->
|
||||
val isSelected = selectedOption == option
|
||||
val backgroundColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
Color.Transparent
|
||||
val contentColor = if (isSelected)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(backgroundColor)
|
||||
.clickable {
|
||||
selectedOption = option
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
text = listOptions[index].titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
listOptions[index].subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (isSelected)
|
||||
contentColor.copy(alpha = 0.8f)
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RadioButtonChecked,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RadioButtonUnchecked,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
selectedOption?.let { onSelected(it) }
|
||||
dismiss()
|
||||
},
|
||||
enabled = selectedOption != null,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok)
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
dismiss()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
)
|
||||
}
|
||||
) {
|
||||
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
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,14 +785,12 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||
cardAlpha
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
if (systemIsDark) 0.8f else 1f
|
||||
colorScheme.background
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
|
||||
1888
manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt
Normal file
1888
manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -207,13 +207,13 @@ private fun TemplateItem(
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
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)
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context,)
|
||||
if (template.local) {
|
||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "local")
|
||||
} else {
|
||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "remote")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,12 @@ private fun TopBar(
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
package com.sukisu.ultra.ui.screen.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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 com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* 空状态显示组件
|
||||
*/
|
||||
@Composable
|
||||
fun EmptyStateCard(
|
||||
message: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径项目卡片组件
|
||||
*/
|
||||
@Composable
|
||||
fun PathItemCard(
|
||||
path: String,
|
||||
icon: ImageVector,
|
||||
onDelete: () -> Unit,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
isLoading: Boolean = false,
|
||||
additionalInfo: String? = null
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = path,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (additionalInfo != null) {
|
||||
Text(
|
||||
text = additionalInfo,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (onEdit != null) {
|
||||
IconButton(
|
||||
onClick = onEdit,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = stringResource(R.string.edit),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onDelete,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kstat配置项目卡片组件
|
||||
*/
|
||||
@Composable
|
||||
fun KstatConfigItemCard(
|
||||
config: String,
|
||||
onDelete: () -> Unit,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column {
|
||||
val parts = config.split("|")
|
||||
if (parts.isNotEmpty()) {
|
||||
Text(
|
||||
text = parts[0], // 路径
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (parts.size > 1) {
|
||||
Text(
|
||||
text = "${parts.drop(1).joinToString(" ")}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = config,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (onEdit != null) {
|
||||
IconButton(
|
||||
onClick = onEdit,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = stringResource(R.string.edit),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onDelete,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Kstat路径项目卡片组件
|
||||
*/
|
||||
@Composable
|
||||
fun AddKstatPathItemCard(
|
||||
path: String,
|
||||
onDelete: () -> Unit,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
onUpdate: () -> Unit,
|
||||
onUpdateFullClone: () -> Unit,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = path,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (onEdit != null) {
|
||||
IconButton(
|
||||
onClick = onEdit,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = stringResource(R.string.edit),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = onUpdate,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Update,
|
||||
contentDescription = stringResource(R.string.update),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onUpdateFullClone,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(R.string.susfs_update_full_clone),
|
||||
tint = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onDelete,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用功能状态卡片组件
|
||||
*/
|
||||
@Composable
|
||||
fun FeatureStatusCard(
|
||||
feature: SuSFSManager.EnabledFeature,
|
||||
onRefresh: (() -> Unit)? = null,
|
||||
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// 日志配置对话框状态
|
||||
var showLogConfigDialog by remember { mutableStateOf(false) }
|
||||
var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) }
|
||||
|
||||
// 日志配置对话框
|
||||
if (showLogConfigDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLogConfigDialog = false },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_log_config_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_log_config_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_enable_log_label),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Switch(
|
||||
checked = logEnabled,
|
||||
onCheckedChange = { logEnabled = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
if (SuSFSManager.setEnableLog(context, logEnabled)) {
|
||||
onRefresh?.invoke()
|
||||
}
|
||||
showLogConfigDialog = false
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.susfs_apply))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
// 恢复原始状态
|
||||
logEnabled = SuSFSManager.getEnableLogState(context)
|
||||
showLogConfigDialog = false
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.then(
|
||||
if (feature.canConfigure) {
|
||||
Modifier.clickable {
|
||||
// 更新当前状态
|
||||
logEnabled = SuSFSManager.getEnableLogState(context)
|
||||
showLogConfigDialog = true
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (feature.canConfigure) {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_feature_configurable),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 状态标签
|
||||
Surface(
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
color = when {
|
||||
feature.isEnabled -> MaterialTheme.colorScheme.primary
|
||||
else -> Color.Gray
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = feature.statusText,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = when {
|
||||
feature.isEnabled -> MaterialTheme.colorScheme.onPrimary
|
||||
else -> Color.White
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SUS挂载隐藏控制卡片组件
|
||||
*/
|
||||
@Composable
|
||||
fun SusMountHidingControlCard(
|
||||
hideSusMountsForAllProcs: Boolean,
|
||||
isLoading: Boolean,
|
||||
onToggleHiding: (Boolean) -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// 标题行
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_hide_mounts_control_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// 描述文本
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_hide_mounts_control_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
|
||||
// 控制开关行
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = if (hideSusMountsForAllProcs) {
|
||||
stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description)
|
||||
} else {
|
||||
stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = hideSusMountsForAllProcs,
|
||||
onCheckedChange = onToggleHiding,
|
||||
enabled = !isLoading
|
||||
)
|
||||
}
|
||||
|
||||
// 当前设置显示
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.susfs_hide_mounts_current_setting,
|
||||
if (hideSusMountsForAllProcs) {
|
||||
stringResource(R.string.susfs_hide_mounts_setting_all)
|
||||
} else {
|
||||
stringResource(R.string.susfs_hide_mounts_setting_non_ksu)
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
// 建议文本
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.susfs_hide_mounts_recommendation),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
lineHeight = 14.sp,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,23 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
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 cardAlpha by mutableFloatStateOf(1f)
|
||||
// 卡片亮度
|
||||
var cardDim by mutableFloatStateOf(0f)
|
||||
// 卡片阴影
|
||||
var cardElevation by mutableStateOf(0.dp)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isCustomDimSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
@@ -31,9 +33,11 @@ object CardConfig {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putFloat("card_dim", cardDim)
|
||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
apply()
|
||||
@@ -46,9 +50,11 @@ object CardConfig {
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||
cardDim = prefs.getFloat("card_dim", 0f)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
@@ -59,22 +65,19 @@ object CardConfig {
|
||||
*/
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||
customBackgroundElevation
|
||||
} else if (enabled) {
|
||||
settingElevation
|
||||
} else {
|
||||
customBackgroundElevation
|
||||
}
|
||||
cardElevation = 0.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深色模式默认值
|
||||
* 设置主题模式默认值
|
||||
*/
|
||||
fun setDarkModeDefaults() {
|
||||
fun setThemeDefaults(isDarkMode: Boolean) {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = if (isDarkMode) 0.5f else 0f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
}
|
||||
@@ -88,6 +91,19 @@ fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||
contentColor = determineContentColor(originalColor)
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取卡片阴影配置
|
||||
*/
|
||||
@Composable
|
||||
fun getCardElevation() = CardDefaults.cardElevation(
|
||||
defaultElevation = CardConfig.cardElevation,
|
||||
pressedElevation = CardConfig.cardElevation,
|
||||
focusedElevation = CardConfig.cardElevation,
|
||||
hoveredElevation = CardConfig.cardElevation,
|
||||
draggedElevation = CardConfig.cardElevation,
|
||||
disabledElevation = CardConfig.cardElevation
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||
*/
|
||||
|
||||
@@ -3,260 +3,602 @@ 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
|
||||
// 浅色
|
||||
abstract val primaryLight: Color
|
||||
abstract val onPrimaryLight: Color
|
||||
abstract val primaryContainerLight: Color
|
||||
abstract val onPrimaryContainerLight: Color
|
||||
abstract val secondaryLight: Color
|
||||
abstract val onSecondaryLight: Color
|
||||
abstract val secondaryContainerLight: Color
|
||||
abstract val onSecondaryContainerLight: Color
|
||||
abstract val tertiaryLight: Color
|
||||
abstract val onTertiaryLight: Color
|
||||
abstract val tertiaryContainerLight: Color
|
||||
abstract val onTertiaryContainerLight: Color
|
||||
abstract val errorLight: Color
|
||||
abstract val onErrorLight: Color
|
||||
abstract val errorContainerLight: Color
|
||||
abstract val onErrorContainerLight: Color
|
||||
abstract val backgroundLight: Color
|
||||
abstract val onBackgroundLight: Color
|
||||
abstract val surfaceLight: Color
|
||||
abstract val onSurfaceLight: Color
|
||||
abstract val surfaceVariantLight: Color
|
||||
abstract val onSurfaceVariantLight: Color
|
||||
abstract val outlineLight: Color
|
||||
abstract val outlineVariantLight: Color
|
||||
abstract val scrimLight: Color
|
||||
abstract val inverseSurfaceLight: Color
|
||||
abstract val inverseOnSurfaceLight: Color
|
||||
abstract val inversePrimaryLight: Color
|
||||
abstract val surfaceDimLight: Color
|
||||
abstract val surfaceBrightLight: Color
|
||||
abstract val surfaceContainerLowestLight: Color
|
||||
abstract val surfaceContainerLowLight: Color
|
||||
abstract val surfaceContainerLight: Color
|
||||
abstract val surfaceContainerHighLight: Color
|
||||
abstract val surfaceContainerHighestLight: Color
|
||||
// 深色
|
||||
abstract val primaryDark: Color
|
||||
abstract val onPrimaryDark: Color
|
||||
abstract val primaryContainerDark: Color
|
||||
abstract val onPrimaryContainerDark: Color
|
||||
abstract val secondaryDark: Color
|
||||
abstract val onSecondaryDark: Color
|
||||
abstract val secondaryContainerDark: Color
|
||||
abstract val onSecondaryContainerDark: Color
|
||||
abstract val tertiaryDark: Color
|
||||
abstract val onTertiaryDark: Color
|
||||
abstract val tertiaryContainerDark: Color
|
||||
abstract val onTertiaryContainerDark: Color
|
||||
abstract val errorDark: Color
|
||||
abstract val onErrorDark: Color
|
||||
abstract val errorContainerDark: Color
|
||||
abstract val onErrorContainerDark: Color
|
||||
abstract val backgroundDark: Color
|
||||
abstract val onBackgroundDark: Color
|
||||
abstract val surfaceDark: Color
|
||||
abstract val onSurfaceDark: Color
|
||||
abstract val surfaceVariantDark: Color
|
||||
abstract val onSurfaceVariantDark: Color
|
||||
abstract val outlineDark: Color
|
||||
abstract val outlineVariantDark: Color
|
||||
abstract val scrimDark: Color
|
||||
abstract val inverseSurfaceDark: Color
|
||||
abstract val inverseOnSurfaceDark: Color
|
||||
abstract val inversePrimaryDark: Color
|
||||
abstract val surfaceDimDark: Color
|
||||
abstract val surfaceBrightDark: Color
|
||||
abstract val surfaceContainerLowestDark: Color
|
||||
abstract val surfaceContainerLowDark: Color
|
||||
abstract val surfaceContainerDark: Color
|
||||
abstract val surfaceContainerHighDark: Color
|
||||
abstract val surfaceContainerHighestDark: 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 primaryLight = Color(0xFF415F91)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFD6E3FF)
|
||||
override val onPrimaryContainerLight = Color(0xFF284777)
|
||||
override val secondaryLight = Color(0xFF565F71)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||
override val onSecondaryContainerLight = Color(0xFF3E4759)
|
||||
override val tertiaryLight = Color(0xFF705575)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFAD8FD)
|
||||
override val onTertiaryContainerLight = Color(0xFF573E5C)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFF9F9FF)
|
||||
override val onBackgroundLight = Color(0xFF191C20)
|
||||
override val surfaceLight = Color(0xFFF9F9FF)
|
||||
override val onSurfaceLight = Color(0xFF191C20)
|
||||
override val surfaceVariantLight = Color(0xFFE0E2EC)
|
||||
override val onSurfaceVariantLight = Color(0xFF44474E)
|
||||
override val outlineLight = Color(0xFF74777F)
|
||||
override val outlineVariantLight = Color(0xFFC4C6D0)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF2E3036)
|
||||
override val inverseOnSurfaceLight = Color(0xFFF0F0F7)
|
||||
override val inversePrimaryLight = Color(0xFFAAC7FF)
|
||||
override val surfaceDimLight = Color(0xFFD9D9E0)
|
||||
override val surfaceBrightLight = Color(0xFFF9F9FF)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFF3F3FA)
|
||||
override val surfaceContainerLight = Color(0xFFEDEDF4)
|
||||
override val surfaceContainerHighLight = Color(0xFFE7E8EE)
|
||||
override val surfaceContainerHighestLight = Color(0xFFE2E2E9)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFAAC7FF)
|
||||
override val onPrimaryDark = Color(0xFF0A305F)
|
||||
override val primaryContainerDark = Color(0xFF284777)
|
||||
override val onPrimaryContainerDark = Color(0xFFD6E3FF)
|
||||
override val secondaryDark = Color(0xFFBEC6DC)
|
||||
override val onSecondaryDark = Color(0xFF283141)
|
||||
override val secondaryContainerDark = Color(0xFF3E4759)
|
||||
override val onSecondaryContainerDark = Color(0xFFDAE2F9)
|
||||
override val tertiaryDark = Color(0xFFDDBCE0)
|
||||
override val onTertiaryDark = Color(0xFF3F2844)
|
||||
override val tertiaryContainerDark = Color(0xFF573E5C)
|
||||
override val onTertiaryContainerDark = Color(0xFFFAD8FD)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF111318)
|
||||
override val onBackgroundDark = Color(0xFFE2E2E9)
|
||||
override val surfaceDark = Color(0xFF111318)
|
||||
override val onSurfaceDark = Color(0xFFE2E2E9)
|
||||
override val surfaceVariantDark = Color(0xFF44474E)
|
||||
override val onSurfaceVariantDark = Color(0xFFC4C6D0)
|
||||
override val outlineDark = Color(0xFF8E9099)
|
||||
override val outlineVariantDark = Color(0xFF44474E)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFE2E2E9)
|
||||
override val inverseOnSurfaceDark = Color(0xFF2E3036)
|
||||
override val inversePrimaryDark = Color(0xFF415F91)
|
||||
override val surfaceDimDark = Color(0xFF111318)
|
||||
override val surfaceBrightDark = Color(0xFF37393E)
|
||||
override val surfaceContainerLowestDark = Color(0xFF0C0E13)
|
||||
override val surfaceContainerLowDark = Color(0xFF191C20)
|
||||
override val surfaceContainerDark = Color(0xFF1D2024)
|
||||
override val surfaceContainerHighDark = Color(0xFF282A2F)
|
||||
override val surfaceContainerHighestDark = Color(0xFF33353A)
|
||||
}
|
||||
|
||||
// 绿色主题
|
||||
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 primaryLight = Color(0xFF4C662B)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFCDEDA3)
|
||||
override val onPrimaryContainerLight = Color(0xFF354E16)
|
||||
override val secondaryLight = Color(0xFF586249)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||
override val onSecondaryContainerLight = Color(0xFF404A33)
|
||||
override val tertiaryLight = Color(0xFF386663)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||
override val onTertiaryContainerLight = Color(0xFF1F4E4B)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFF9FAEF)
|
||||
override val onBackgroundLight = Color(0xFF1A1C16)
|
||||
override val surfaceLight = Color(0xFFF9FAEF)
|
||||
override val onSurfaceLight = Color(0xFF1A1C16)
|
||||
override val surfaceVariantLight = Color(0xFFE1E4D5)
|
||||
override val onSurfaceVariantLight = Color(0xFF44483D)
|
||||
override val outlineLight = Color(0xFF75796C)
|
||||
override val outlineVariantLight = Color(0xFFC5C8BA)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF2F312A)
|
||||
override val inverseOnSurfaceLight = Color(0xFFF1F2E6)
|
||||
override val inversePrimaryLight = Color(0xFFB1D18A)
|
||||
override val surfaceDimLight = Color(0xFFDADBD0)
|
||||
override val surfaceBrightLight = Color(0xFFF9FAEF)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFF3F4E9)
|
||||
override val surfaceContainerLight = Color(0xFFEEEFE3)
|
||||
override val surfaceContainerHighLight = Color(0xFFE8E9DE)
|
||||
override val surfaceContainerHighestLight = Color(0xFFE2E3D8)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFB1D18A)
|
||||
override val onPrimaryDark = Color(0xFF1F3701)
|
||||
override val primaryContainerDark = Color(0xFF354E16)
|
||||
override val onPrimaryContainerDark = Color(0xFFCDEDA3)
|
||||
override val secondaryDark = Color(0xFFBFCBAD)
|
||||
override val onSecondaryDark = Color(0xFF2A331E)
|
||||
override val secondaryContainerDark = Color(0xFF404A33)
|
||||
override val onSecondaryContainerDark = Color(0xFFDCE7C8)
|
||||
override val tertiaryDark = Color(0xFFA0D0CB)
|
||||
override val onTertiaryDark = Color(0xFF003735)
|
||||
override val tertiaryContainerDark = Color(0xFF1F4E4B)
|
||||
override val onTertiaryContainerDark = Color(0xFFBCECE7)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF12140E)
|
||||
override val onBackgroundDark = Color(0xFFE2E3D8)
|
||||
override val surfaceDark = Color(0xFF12140E)
|
||||
override val onSurfaceDark = Color(0xFFE2E3D8)
|
||||
override val surfaceVariantDark = Color(0xFF44483D)
|
||||
override val onSurfaceVariantDark = Color(0xFFC5C8BA)
|
||||
override val outlineDark = Color(0xFF8F9285)
|
||||
override val outlineVariantDark = Color(0xFF44483D)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFE2E3D8)
|
||||
override val inverseOnSurfaceDark = Color(0xFF2F312A)
|
||||
override val inversePrimaryDark = Color(0xFF4C662B)
|
||||
override val surfaceDimDark = Color(0xFF12140E)
|
||||
override val surfaceBrightDark = Color(0xFF383A32)
|
||||
override val surfaceContainerLowestDark = Color(0xFF0C0F09)
|
||||
override val surfaceContainerLowDark = Color(0xFF1A1C16)
|
||||
override val surfaceContainerDark = Color(0xFF1E201A)
|
||||
override val surfaceContainerHighDark = Color(0xFF282B24)
|
||||
override val surfaceContainerHighestDark = Color(0xFF33362E)
|
||||
}
|
||||
|
||||
// 紫色主题
|
||||
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 primaryLight = Color(0xFF7C4E7E)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFD6FC)
|
||||
override val onPrimaryContainerLight = Color(0xFF623765)
|
||||
override val secondaryLight = Color(0xFF6C586B)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFF5DBF1)
|
||||
override val onSecondaryContainerLight = Color(0xFF534152)
|
||||
override val tertiaryLight = Color(0xFF825249)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFDAD4)
|
||||
override val onTertiaryContainerLight = Color(0xFF673B33)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFFFF7FA)
|
||||
override val onBackgroundLight = Color(0xFF1F1A1F)
|
||||
override val surfaceLight = Color(0xFFFFF7FA)
|
||||
override val onSurfaceLight = Color(0xFF1F1A1F)
|
||||
override val surfaceVariantLight = Color(0xFFEDDFE8)
|
||||
override val onSurfaceVariantLight = Color(0xFF4D444C)
|
||||
override val outlineLight = Color(0xFF7F747C)
|
||||
override val outlineVariantLight = Color(0xFFD0C3CC)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF352F34)
|
||||
override val inverseOnSurfaceLight = Color(0xFFF9EEF4)
|
||||
override val inversePrimaryLight = Color(0xFFECB4EC)
|
||||
override val surfaceDimLight = Color(0xFFE2D7DE)
|
||||
override val surfaceBrightLight = Color(0xFFFFF7FA)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFFCF0F7)
|
||||
override val surfaceContainerLight = Color(0xFFF6EBF2)
|
||||
override val surfaceContainerHighLight = Color(0xFFF0E5EC)
|
||||
override val surfaceContainerHighestLight = Color(0xFFEBDFE6)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFECB4EC)
|
||||
override val onPrimaryDark = Color(0xFF49204D)
|
||||
override val primaryContainerDark = Color(0xFF623765)
|
||||
override val onPrimaryContainerDark = Color(0xFFFFD6FC)
|
||||
override val secondaryDark = Color(0xFFD8BFD5)
|
||||
override val onSecondaryDark = Color(0xFF3B2B3B)
|
||||
override val secondaryContainerDark = Color(0xFF534152)
|
||||
override val onSecondaryContainerDark = Color(0xFFF5DBF1)
|
||||
override val tertiaryDark = Color(0xFFF6B8AD)
|
||||
override val onTertiaryDark = Color(0xFF4C251F)
|
||||
override val tertiaryContainerDark = Color(0xFF673B33)
|
||||
override val onTertiaryContainerDark = Color(0xFFFFDAD4)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF171216)
|
||||
override val onBackgroundDark = Color(0xFFEBDFE6)
|
||||
override val surfaceDark = Color(0xFF171216)
|
||||
override val onSurfaceDark = Color(0xFFEBDFE6)
|
||||
override val surfaceVariantDark = Color(0xFF4D444C)
|
||||
override val onSurfaceVariantDark = Color(0xFFD0C3CC)
|
||||
override val outlineDark = Color(0xFF998D96)
|
||||
override val outlineVariantDark = Color(0xFF4D444C)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFEBDFE6)
|
||||
override val inverseOnSurfaceDark = Color(0xFF352F34)
|
||||
override val inversePrimaryDark = Color(0xFF7C4E7E)
|
||||
override val surfaceDimDark = Color(0xFF171216)
|
||||
override val surfaceBrightDark = Color(0xFF3E373D)
|
||||
override val surfaceContainerLowestDark = Color(0xFF110D11)
|
||||
override val surfaceContainerLowDark = Color(0xFF1F1A1F)
|
||||
override val surfaceContainerDark = Color(0xFF231E23)
|
||||
override val surfaceContainerHighDark = Color(0xFF2E282D)
|
||||
override val surfaceContainerHighestDark = Color(0xFF393338)
|
||||
}
|
||||
|
||||
// 橙色主题
|
||||
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 primaryLight = Color(0xFF8B4F24)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFDCC7)
|
||||
override val onPrimaryContainerLight = Color(0xFF6E390E)
|
||||
override val secondaryLight = Color(0xFF755846)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFFFDCC7)
|
||||
override val onSecondaryContainerLight = Color(0xFF5B4130)
|
||||
override val tertiaryLight = Color(0xFF865219)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFDCBF)
|
||||
override val onTertiaryContainerLight = Color(0xFF6A3B01)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFFFF8F5)
|
||||
override val onBackgroundLight = Color(0xFF221A15)
|
||||
override val surfaceLight = Color(0xFFFFF8F5)
|
||||
override val onSurfaceLight = Color(0xFF221A15)
|
||||
override val surfaceVariantLight = Color(0xFFF4DED3)
|
||||
override val onSurfaceVariantLight = Color(0xFF52443C)
|
||||
override val outlineLight = Color(0xFF84746A)
|
||||
override val outlineVariantLight = Color(0xFFD7C3B8)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF382E29)
|
||||
override val inverseOnSurfaceLight = Color(0xFFFFEDE5)
|
||||
override val inversePrimaryLight = Color(0xFFFFB787)
|
||||
override val surfaceDimLight = Color(0xFFE7D7CE)
|
||||
override val surfaceBrightLight = Color(0xFFFFF8F5)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFFFF1EA)
|
||||
override val surfaceContainerLight = Color(0xFFFCEBE2)
|
||||
override val surfaceContainerHighLight = Color(0xFFF6E5DC)
|
||||
override val surfaceContainerHighestLight = Color(0xFFF0DFD7)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFFFB787)
|
||||
override val onPrimaryDark = Color(0xFF502400)
|
||||
override val primaryContainerDark = Color(0xFF6E390E)
|
||||
override val onPrimaryContainerDark = Color(0xFFFFDCC7)
|
||||
override val secondaryDark = Color(0xFFE5BFA8)
|
||||
override val onSecondaryDark = Color(0xFF422B1B)
|
||||
override val secondaryContainerDark = Color(0xFF5B4130)
|
||||
override val onSecondaryContainerDark = Color(0xFFFFDCC7)
|
||||
override val tertiaryDark = Color(0xFFFDB876)
|
||||
override val onTertiaryDark = Color(0xFF4B2800)
|
||||
override val tertiaryContainerDark = Color(0xFF6A3B01)
|
||||
override val onTertiaryContainerDark = Color(0xFFFFDCBF)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF19120D)
|
||||
override val onBackgroundDark = Color(0xFFF0DFD7)
|
||||
override val surfaceDark = Color(0xFF19120D)
|
||||
override val onSurfaceDark = Color(0xFFF0DFD7)
|
||||
override val surfaceVariantDark = Color(0xFF52443C)
|
||||
override val onSurfaceVariantDark = Color(0xFFD7C3B8)
|
||||
override val outlineDark = Color(0xFF9F8D83)
|
||||
override val outlineVariantDark = Color(0xFF52443C)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFF0DFD7)
|
||||
override val inverseOnSurfaceDark = Color(0xFF382E29)
|
||||
override val inversePrimaryDark = Color(0xFF8B4F24)
|
||||
override val surfaceDimDark = Color(0xFF19120D)
|
||||
override val surfaceBrightDark = Color(0xFF413731)
|
||||
override val surfaceContainerLowestDark = Color(0xFF140D08)
|
||||
override val surfaceContainerLowDark = Color(0xFF221A15)
|
||||
override val surfaceContainerDark = Color(0xFF261E19)
|
||||
override val surfaceContainerHighDark = Color(0xFF312823)
|
||||
override val surfaceContainerHighestDark = Color(0xFF3D332D)
|
||||
}
|
||||
|
||||
// 粉色主题
|
||||
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 primaryLight = Color(0xFF8C4A60)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFFFD9E2)
|
||||
override val onPrimaryContainerLight = Color(0xFF703348)
|
||||
override val secondaryLight = Color(0xFF8B4A62)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFFFD9E3)
|
||||
override val onSecondaryContainerLight = Color(0xFF6F334B)
|
||||
override val tertiaryLight = Color(0xFF8B4A62)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFFFD9E3)
|
||||
override val onTertiaryContainerLight = Color(0xFF6F334B)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFFFF8F8)
|
||||
override val onBackgroundLight = Color(0xFF22191B)
|
||||
override val surfaceLight = Color(0xFFFFF8F8)
|
||||
override val onSurfaceLight = Color(0xFF22191B)
|
||||
override val surfaceVariantLight = Color(0xFFF2DDE1)
|
||||
override val onSurfaceVariantLight = Color(0xFF514346)
|
||||
override val outlineLight = Color(0xFF837377)
|
||||
override val outlineVariantLight = Color(0xFFD5C2C5)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF372E30)
|
||||
override val inverseOnSurfaceLight = Color(0xFFFDEDEF)
|
||||
override val inversePrimaryLight = Color(0xFFFFB1C7)
|
||||
override val surfaceDimLight = Color(0xFFE6D6D9)
|
||||
override val surfaceBrightLight = Color(0xFFFFF8F8)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFFFF0F2)
|
||||
override val surfaceContainerLight = Color(0xFFFBEAED)
|
||||
override val surfaceContainerHighLight = Color(0xFFF5E4E7)
|
||||
override val surfaceContainerHighestLight = Color(0xFFEFDFE1)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFFFB1C7)
|
||||
override val onPrimaryDark = Color(0xFF541D32)
|
||||
override val primaryContainerDark = Color(0xFF703348)
|
||||
override val onPrimaryContainerDark = Color(0xFFFFD9E2)
|
||||
override val secondaryDark = Color(0xFFFFB0CB)
|
||||
override val onSecondaryDark = Color(0xFF541D34)
|
||||
override val secondaryContainerDark = Color(0xFF6F334B)
|
||||
override val onSecondaryContainerDark = Color(0xFFFFD9E3)
|
||||
override val tertiaryDark = Color(0xFFFFB0CB)
|
||||
override val onTertiaryDark = Color(0xFF541D34)
|
||||
override val tertiaryContainerDark = Color(0xFF6F334B)
|
||||
override val onTertiaryContainerDark = Color(0xFFFFD9E3)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF191113)
|
||||
override val onBackgroundDark = Color(0xFFEFDFE1)
|
||||
override val surfaceDark = Color(0xFF191113)
|
||||
override val onSurfaceDark = Color(0xFFEFDFE1)
|
||||
override val surfaceVariantDark = Color(0xFF514346)
|
||||
override val onSurfaceVariantDark = Color(0xFFD5C2C5)
|
||||
override val outlineDark = Color(0xFF9E8C90)
|
||||
override val outlineVariantDark = Color(0xFF514346)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFEFDFE1)
|
||||
override val inverseOnSurfaceDark = Color(0xFF372E30)
|
||||
override val inversePrimaryDark = Color(0xFF8C4A60)
|
||||
override val surfaceDimDark = Color(0xFF191113)
|
||||
override val surfaceBrightDark = Color(0xFF413739)
|
||||
override val surfaceContainerLowestDark = Color(0xFF140C0E)
|
||||
override val surfaceContainerLowDark = Color(0xFF22191B)
|
||||
override val surfaceContainerDark = Color(0xFF261D1F)
|
||||
override val surfaceContainerHighDark = Color(0xFF31282A)
|
||||
override val surfaceContainerHighestDark = Color(0xFF3C3234)
|
||||
}
|
||||
|
||||
// 灰色主题
|
||||
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 primaryLight = Color(0xFF5B5C5C)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFF747474)
|
||||
override val onPrimaryContainerLight = Color(0xFFFEFCFC)
|
||||
override val secondaryLight = Color(0xFF5F5E5E)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFE4E2E1)
|
||||
override val onSecondaryContainerLight = Color(0xFF656464)
|
||||
override val tertiaryLight = Color(0xFF5E5B5D)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFF777375)
|
||||
override val onTertiaryContainerLight = Color(0xFFFFFBFF)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFFCF8F8)
|
||||
override val onBackgroundLight = Color(0xFF1C1B1B)
|
||||
override val surfaceLight = Color(0xFFFCF8F8)
|
||||
override val onSurfaceLight = Color(0xFF1C1B1B)
|
||||
override val surfaceVariantLight = Color(0xFFE0E3E3)
|
||||
override val onSurfaceVariantLight = Color(0xFF444748)
|
||||
override val outlineLight = Color(0xFF747878)
|
||||
override val outlineVariantLight = Color(0xFFC4C7C7)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF313030)
|
||||
override val inverseOnSurfaceLight = Color(0xFFF4F0EF)
|
||||
override val inversePrimaryLight = Color(0xFFC7C6C6)
|
||||
override val surfaceDimLight = Color(0xFFDDD9D8)
|
||||
override val surfaceBrightLight = Color(0xFFFCF8F8)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFF7F3F2)
|
||||
override val surfaceContainerLight = Color(0xFFF1EDEC)
|
||||
override val surfaceContainerHighLight = Color(0xFFEBE7E7)
|
||||
override val surfaceContainerHighestLight = Color(0xFFE5E2E1)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFC7C6C6)
|
||||
override val onPrimaryDark = Color(0xFF303031)
|
||||
override val primaryContainerDark = Color(0xFF919190)
|
||||
override val onPrimaryContainerDark = Color(0xFF161718)
|
||||
override val secondaryDark = Color(0xFFC8C6C5)
|
||||
override val onSecondaryDark = Color(0xFF303030)
|
||||
override val secondaryContainerDark = Color(0xFF474746)
|
||||
override val onSecondaryContainerDark = Color(0xFFB7B5B4)
|
||||
override val tertiaryDark = Color(0xFFCAC5C7)
|
||||
override val onTertiaryDark = Color(0xFF323031)
|
||||
override val tertiaryContainerDark = Color(0xFF948F91)
|
||||
override val onTertiaryContainerDark = Color(0xFF181718)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF141313)
|
||||
override val onBackgroundDark = Color(0xFFE5E2E1)
|
||||
override val surfaceDark = Color(0xFF141313)
|
||||
override val onSurfaceDark = Color(0xFFE5E2E1)
|
||||
override val surfaceVariantDark = Color(0xFF444748)
|
||||
override val onSurfaceVariantDark = Color(0xFFC4C7C7)
|
||||
override val outlineDark = Color(0xFF8E9192)
|
||||
override val outlineVariantDark = Color(0xFF444748)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFE5E2E1)
|
||||
override val inverseOnSurfaceDark = Color(0xFF313030)
|
||||
override val inversePrimaryDark = Color(0xFF5E5E5E)
|
||||
override val surfaceDimDark = Color(0xFF141313)
|
||||
override val surfaceBrightDark = Color(0xFF3A3939)
|
||||
override val surfaceContainerLowestDark = Color(0xFF0E0E0E)
|
||||
override val surfaceContainerLowDark = Color(0xFF1C1B1B)
|
||||
override val surfaceContainerDark = Color(0xFF201F1F)
|
||||
override val surfaceContainerHighDark = Color(0xFF2A2A2A)
|
||||
override val surfaceContainerHighestDark = Color(0xFF353434)
|
||||
}
|
||||
|
||||
// 黄色主题
|
||||
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 primaryLight = Color(0xFF6D5E0F)
|
||||
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
override val primaryContainerLight = Color(0xFFF8E288)
|
||||
override val onPrimaryContainerLight = Color(0xFF534600)
|
||||
override val secondaryLight = Color(0xFF6D5E0F)
|
||||
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
override val secondaryContainerLight = Color(0xFFF7E388)
|
||||
override val onSecondaryContainerLight = Color(0xFF534600)
|
||||
override val tertiaryLight = Color(0xFF685F13)
|
||||
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
override val tertiaryContainerLight = Color(0xFFF1E58A)
|
||||
override val onTertiaryContainerLight = Color(0xFF4F4800)
|
||||
override val errorLight = Color(0xFFBA1A1A)
|
||||
override val onErrorLight = Color(0xFFFFFFFF)
|
||||
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||
override val onErrorContainerLight = Color(0xFF93000A)
|
||||
override val backgroundLight = Color(0xFFFFF9ED)
|
||||
override val onBackgroundLight = Color(0xFF1E1C13)
|
||||
override val surfaceLight = Color(0xFFFFF9ED)
|
||||
override val onSurfaceLight = Color(0xFF1E1C13)
|
||||
override val surfaceVariantLight = Color(0xFFE9E2D0)
|
||||
override val onSurfaceVariantLight = Color(0xFF4B4739)
|
||||
override val outlineLight = Color(0xFF7C7768)
|
||||
override val outlineVariantLight = Color(0xFFCDC6B4)
|
||||
override val scrimLight = Color(0xFF000000)
|
||||
override val inverseSurfaceLight = Color(0xFF333027)
|
||||
override val inverseOnSurfaceLight = Color(0xFFF7F0E2)
|
||||
override val inversePrimaryLight = Color(0xFFDAC66F)
|
||||
override val surfaceDimLight = Color(0xFFE0D9CC)
|
||||
override val surfaceBrightLight = Color(0xFFFFF9ED)
|
||||
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
override val surfaceContainerLowLight = Color(0xFFFAF3E5)
|
||||
override val surfaceContainerLight = Color(0xFFF4EDDF)
|
||||
override val surfaceContainerHighLight = Color(0xFFEEE8DA)
|
||||
override val surfaceContainerHighestLight = Color(0xFFE8E2D4)
|
||||
|
||||
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)
|
||||
override val primaryDark = Color(0xFFDAC66F)
|
||||
override val onPrimaryDark = Color(0xFF393000)
|
||||
override val primaryContainerDark = Color(0xFF534600)
|
||||
override val onPrimaryContainerDark = Color(0xFFF8E288)
|
||||
override val secondaryDark = Color(0xFFDAC76F)
|
||||
override val onSecondaryDark = Color(0xFF393000)
|
||||
override val secondaryContainerDark = Color(0xFF534600)
|
||||
override val onSecondaryContainerDark = Color(0xFFF7E388)
|
||||
override val tertiaryDark = Color(0xFFD4C871)
|
||||
override val onTertiaryDark = Color(0xFF363100)
|
||||
override val tertiaryContainerDark = Color(0xFF4F4800)
|
||||
override val onTertiaryContainerDark = Color(0xFFF1E58A)
|
||||
override val errorDark = Color(0xFFFFB4AB)
|
||||
override val onErrorDark = Color(0xFF690005)
|
||||
override val errorContainerDark = Color(0xFF93000A)
|
||||
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
override val backgroundDark = Color(0xFF15130B)
|
||||
override val onBackgroundDark = Color(0xFFE8E2D4)
|
||||
override val surfaceDark = Color(0xFF15130B)
|
||||
override val onSurfaceDark = Color(0xFFE8E2D4)
|
||||
override val surfaceVariantDark = Color(0xFF4B4739)
|
||||
override val onSurfaceVariantDark = Color(0xFFCDC6B4)
|
||||
override val outlineDark = Color(0xFF969080)
|
||||
override val outlineVariantDark = Color(0xFF4B4739)
|
||||
override val scrimDark = Color(0xFF000000)
|
||||
override val inverseSurfaceDark = Color(0xFFE8E2D4)
|
||||
override val inverseOnSurfaceDark = Color(0xFF333027)
|
||||
override val inversePrimaryDark = Color(0xFF6D5E0F)
|
||||
override val surfaceDimDark = Color(0xFF15130B)
|
||||
override val surfaceBrightDark = Color(0xFF3C3930)
|
||||
override val surfaceContainerLowestDark = Color(0xFF100E07)
|
||||
override val surfaceContainerLowDark = Color(0xFF1E1C13)
|
||||
override val surfaceContainerDark = Color(0xFF222017)
|
||||
override val surfaceContainerHighDark = Color(0xFF2C2A21)
|
||||
override val surfaceContainerHighestDark = Color(0xFF37352B)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -44,6 +44,12 @@ import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
||||
/**
|
||||
* 主题配置对象,管理应用的主题相关状态
|
||||
@@ -105,11 +111,18 @@ fun KernelSUTheme(
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = if (systemIsDark) 0.5f else 0f
|
||||
}
|
||||
save(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarStyle(
|
||||
darkMode = darkTheme
|
||||
)
|
||||
|
||||
// 初始加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
context.loadThemeMode()
|
||||
@@ -138,7 +151,9 @@ fun KernelSUTheme(
|
||||
// 根据暗色模式和自定义背景调整卡片配置
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
CardConfig.setThemeDefaults(true)
|
||||
} else if (!darkTheme && !dynamicColor) {
|
||||
CardConfig.setThemeDefaults(false)
|
||||
}
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
@@ -190,6 +205,9 @@ fun KernelSUTheme(
|
||||
}
|
||||
}
|
||||
|
||||
// 计算适用的暗化值
|
||||
val dimFactor = CardConfig.cardDim
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
@@ -199,7 +217,8 @@ fun KernelSUTheme(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
.background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
|
||||
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
|
||||
)
|
||||
|
||||
// 自定义背景层
|
||||
@@ -225,13 +244,13 @@ fun KernelSUTheme(
|
||||
)
|
||||
}
|
||||
|
||||
// 亮度调节层
|
||||
// 亮度调节层 (根据cardDim调整)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||
else Color.White.copy(alpha = 0.1f)
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -243,8 +262,8 @@ fun KernelSUTheme(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||
else Color.Black.copy(alpha = 0.2f)
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
@@ -270,54 +289,73 @@ fun KernelSUTheme(
|
||||
*/
|
||||
@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
|
||||
private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
|
||||
val scheme = dynamicDarkColorScheme(context)
|
||||
return scheme.copy(
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
|
||||
onBackground = scheme.onBackground,
|
||||
onSurface = scheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态浅色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicLightColorScheme(context: Context) =
|
||||
dynamicLightColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
private fun createDynamicLightColorScheme(context: Context): ColorScheme {
|
||||
val scheme = dynamicLightColorScheme(context)
|
||||
return scheme.copy(
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
|
||||
onBackground = scheme.onBackground,
|
||||
onSurface = scheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建深色颜色方案
|
||||
*/
|
||||
@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
|
||||
primary = ThemeConfig.currentTheme.primaryDark,
|
||||
onPrimary = ThemeConfig.currentTheme.onPrimaryDark,
|
||||
primaryContainer = ThemeConfig.currentTheme.primaryContainerDark,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerDark,
|
||||
secondary = ThemeConfig.currentTheme.secondaryDark,
|
||||
onSecondary = ThemeConfig.currentTheme.onSecondaryDark,
|
||||
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerDark,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerDark,
|
||||
tertiary = ThemeConfig.currentTheme.tertiaryDark,
|
||||
onTertiary = ThemeConfig.currentTheme.onTertiaryDark,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerDark,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerDark,
|
||||
error = ThemeConfig.currentTheme.errorDark,
|
||||
onError = ThemeConfig.currentTheme.onErrorDark,
|
||||
errorContainer = ThemeConfig.currentTheme.errorContainerDark,
|
||||
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark,
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark,
|
||||
onBackground = ThemeConfig.currentTheme.onBackgroundDark,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark,
|
||||
onSurface = ThemeConfig.currentTheme.onSurfaceDark,
|
||||
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark,
|
||||
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark,
|
||||
outline = ThemeConfig.currentTheme.outlineDark,
|
||||
outlineVariant = ThemeConfig.currentTheme.outlineVariantDark,
|
||||
scrim = ThemeConfig.currentTheme.scrimDark,
|
||||
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceDark,
|
||||
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceDark,
|
||||
inversePrimary = ThemeConfig.currentTheme.inversePrimaryDark,
|
||||
surfaceDim = ThemeConfig.currentTheme.surfaceDimDark,
|
||||
surfaceBright = ThemeConfig.currentTheme.surfaceBrightDark,
|
||||
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestDark,
|
||||
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowDark,
|
||||
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerDark,
|
||||
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighDark,
|
||||
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -325,32 +363,44 @@ private fun createDarkColorScheme() = darkColorScheme(
|
||||
*/
|
||||
@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
|
||||
primary = ThemeConfig.currentTheme.primaryLight,
|
||||
onPrimary = ThemeConfig.currentTheme.onPrimaryLight,
|
||||
primaryContainer = ThemeConfig.currentTheme.primaryContainerLight,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerLight,
|
||||
secondary = ThemeConfig.currentTheme.secondaryLight,
|
||||
onSecondary = ThemeConfig.currentTheme.onSecondaryLight,
|
||||
secondaryContainer = ThemeConfig.currentTheme.secondaryContainerLight,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerLight,
|
||||
tertiary = ThemeConfig.currentTheme.tertiaryLight,
|
||||
onTertiary = ThemeConfig.currentTheme.onTertiaryLight,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerLight,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerLight,
|
||||
error = ThemeConfig.currentTheme.errorLight,
|
||||
onError = ThemeConfig.currentTheme.onErrorLight,
|
||||
errorContainer = ThemeConfig.currentTheme.errorContainerLight,
|
||||
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight,
|
||||
background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight,
|
||||
onBackground = ThemeConfig.currentTheme.onBackgroundLight,
|
||||
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight,
|
||||
onSurface = ThemeConfig.currentTheme.onSurfaceLight,
|
||||
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight,
|
||||
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight,
|
||||
outline = ThemeConfig.currentTheme.outlineLight,
|
||||
outlineVariant = ThemeConfig.currentTheme.outlineVariantLight,
|
||||
scrim = ThemeConfig.currentTheme.scrimLight,
|
||||
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceLight,
|
||||
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceLight,
|
||||
inversePrimary = ThemeConfig.currentTheme.inversePrimaryLight,
|
||||
surfaceDim = ThemeConfig.currentTheme.surfaceDimLight,
|
||||
surfaceBright = ThemeConfig.currentTheme.surfaceBrightLight,
|
||||
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestLight,
|
||||
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowLight,
|
||||
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerLight,
|
||||
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighLight,
|
||||
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* 复制图片到应用内部存储并提升持久性
|
||||
*/
|
||||
@@ -536,3 +586,32 @@ fun Context.loadDynamicColorState() {
|
||||
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SystemBarStyle(
|
||||
darkMode: Boolean,
|
||||
statusBarScrim: Color = Color.Transparent,
|
||||
navigationBarScrim: Color = Color.Transparent,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as ComponentActivity
|
||||
|
||||
SideEffect {
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
statusBarScrim.toArgb(),
|
||||
statusBarScrim.toArgb(),
|
||||
) { darkMode },
|
||||
navigationBarStyle = when {
|
||||
darkMode -> SystemBarStyle.dark(
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
|
||||
else -> SystemBarStyle.light(
|
||||
navigationBarScrim.toArgb(),
|
||||
navigationBarScrim.toArgb(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,23 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val TAG = "DownloadUtil"
|
||||
private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
|
||||
private const val MAX_RETRY_COUNT = 3
|
||||
private const val RETRY_DELAY_MS = 3000L
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -26,8 +36,10 @@ fun download(
|
||||
fileName: String,
|
||||
description: String,
|
||||
onDownloaded: (Uri) -> Unit = {},
|
||||
onDownloading: () -> Unit = {}
|
||||
onDownloading: () -> Unit = {},
|
||||
onError: (String) -> Unit = {}
|
||||
) {
|
||||
Log.d(TAG, "Start Download: $url")
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
@@ -49,6 +61,13 @@ fun download(
|
||||
}
|
||||
}
|
||||
}
|
||||
val downloadFile = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
fileName
|
||||
)
|
||||
if (downloadFile.exists()) {
|
||||
downloadFile.delete()
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(url.toUri())
|
||||
.setDestinationInExternalPublicDir(
|
||||
@@ -59,66 +78,206 @@ fun download(
|
||||
.setMimeType("application/zip")
|
||||
.setTitle(fileName)
|
||||
.setDescription(description)
|
||||
.addRequestHeader("User-Agent", CUSTOM_USER_AGENT)
|
||||
.setAllowedOverMetered(true)
|
||||
.setAllowedOverRoaming(true)
|
||||
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||
|
||||
downloadManager.enqueue(request)
|
||||
try {
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
Log.d(TAG, "Successful launch of the download,ID: $downloadId")
|
||||
monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Download startup failure", e)
|
||||
onError("Download startup failure: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun monitorDownload(
|
||||
context: Context,
|
||||
downloadManager: DownloadManager,
|
||||
downloadId: Long,
|
||||
url: String,
|
||||
fileName: String,
|
||||
description: String,
|
||||
onDownloaded: (Uri) -> Unit,
|
||||
onDownloading: () -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
retryCount: Int = 0
|
||||
) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
|
||||
var lastProgress = -1
|
||||
var stuckCounter = 0
|
||||
|
||||
val runnable = object : Runnable {
|
||||
override fun run() {
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
@SuppressLint("Range")
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
|
||||
when (status) {
|
||||
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||
@SuppressLint("Range")
|
||||
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
Log.d(TAG, "Download Successfully: $localUri")
|
||||
onDownloaded(localUri.toUri())
|
||||
return
|
||||
}
|
||||
DownloadManager.STATUS_FAILED -> {
|
||||
@SuppressLint("Range")
|
||||
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||
Log.d(TAG, "Download failed with reason code: $reason")
|
||||
|
||||
if (retryCount < MAX_RETRY_COUNT) {
|
||||
Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}")
|
||||
handler.postDelayed({
|
||||
downloadManager.remove(downloadId)
|
||||
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||
}, RETRY_DELAY_MS)
|
||||
} else {
|
||||
onError("Download failed, please check network connection or storage space")
|
||||
}
|
||||
return
|
||||
}
|
||||
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> {
|
||||
@SuppressLint("Range")
|
||||
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||
@SuppressLint("Range")
|
||||
val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
|
||||
if (totalBytes > 0) {
|
||||
val progress = (downloadedBytes * 100 / totalBytes).toInt()
|
||||
if (progress == lastProgress) {
|
||||
stuckCounter++
|
||||
if (stuckCounter > 30) {
|
||||
if (retryCount < MAX_RETRY_COUNT) {
|
||||
Log.d(TAG, "Download stalled and restarted")
|
||||
downloadManager.remove(downloadId)
|
||||
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastProgress = progress
|
||||
stuckCounter = 0
|
||||
Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
handler.post(runnable)
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
|
||||
if (id == downloadId) {
|
||||
handler.removeCallbacks(runnable)
|
||||
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
downloadManager.query(query).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
@SuppressLint("Range")
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
|
||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
@SuppressLint("Range")
|
||||
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
onDownloaded(localUri.toUri())
|
||||
} else {
|
||||
if (retryCount < MAX_RETRY_COUNT) {
|
||||
download(context!!, url, fileName, description, onDownloaded, onDownloading, onError)
|
||||
} else {
|
||||
onError("Download failed, please try again later")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context?.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
fun checkNewVersion(): LatestVersionInfo {
|
||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||
val defaultValue = LatestVersionInfo()
|
||||
return runCatching {
|
||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
||||
.use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||
return defaultValue
|
||||
}
|
||||
val body = response.body?.string()
|
||||
if (body == null) {
|
||||
Log.d("CheckUpdate", "Response body is null")
|
||||
return defaultValue
|
||||
}
|
||||
Log.d("CheckUpdate", "Response body: $body")
|
||||
val json = org.json.JSONObject(body)
|
||||
val client = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||
val tagName = json.optString("tag_name", "")
|
||||
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||
.build()
|
||||
|
||||
// 从 body 字段获取更新日志(保留换行符)
|
||||
val changelog = json.optString("body")
|
||||
.replace("\\r\\n", "\n") // 转换换行符
|
||||
|
||||
val assets = json.getJSONArray("assets")
|
||||
for (i in 0 until assets.length()) {
|
||||
val asset = assets.getJSONObject(i)
|
||||
val name = asset.getString("name")
|
||||
if (!name.endsWith(".apk")) continue
|
||||
|
||||
// 修改正则表达式,只匹配 SukiSU 和版本号
|
||||
val regex = Regex("SukiSU.*_(\\d+)-release")
|
||||
val matchResult = regex.find(name)
|
||||
if (matchResult == null) {
|
||||
Log.d("CheckUpdate", "No match found in $name, skipping")
|
||||
continue
|
||||
}
|
||||
val versionCode = matchResult.groupValues[1].toInt()
|
||||
|
||||
val downloadUrl = asset.getString("browser_download_url")
|
||||
return LatestVersionInfo(
|
||||
versionCode,
|
||||
downloadUrl,
|
||||
changelog,
|
||||
versionName
|
||||
)
|
||||
}
|
||||
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
|
||||
defaultValue
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.d("CheckUpdate", "Network request failed: ${response.message}")
|
||||
return defaultValue
|
||||
}
|
||||
val body = response.body?.string()
|
||||
if (body == null) {
|
||||
Log.d("CheckUpdate", "Return data is null")
|
||||
return defaultValue
|
||||
}
|
||||
Log.d("CheckUpdate", "Return data: $body")
|
||||
val json = org.json.JSONObject(body)
|
||||
|
||||
// 直接从 tag_name 提取版本号(如 v1.1)
|
||||
val tagName = json.optString("tag_name", "")
|
||||
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
|
||||
|
||||
// 从 body 字段获取更新日志(保留换行符)
|
||||
val changelog = json.optString("body")
|
||||
.replace("\\r\\n", "\n") // 转换换行符
|
||||
|
||||
val assets = json.getJSONArray("assets")
|
||||
for (i in 0 until assets.length()) {
|
||||
val asset = assets.getJSONObject(i)
|
||||
val name = asset.getString("name")
|
||||
if (!name.endsWith(".apk")) continue
|
||||
|
||||
val regex = Regex("SukiSU.*_(\\d+)-release")
|
||||
val matchResult = regex.find(name)
|
||||
if (matchResult == null) {
|
||||
Log.d("CheckUpdate", "No matches found: $name, skip over")
|
||||
continue
|
||||
}
|
||||
val versionCode = matchResult.groupValues[1].toInt()
|
||||
|
||||
val downloadUrl = asset.getString("browser_download_url")
|
||||
return LatestVersionInfo(
|
||||
versionCode,
|
||||
downloadUrl,
|
||||
changelog,
|
||||
versionName
|
||||
)
|
||||
}
|
||||
Log.d("CheckUpdate", "No valid APK resource found, return default value")
|
||||
defaultValue
|
||||
}
|
||||
}.getOrDefault(defaultValue)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
DisposableEffect(context) {
|
||||
@@ -158,4 +317,3 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,9 @@ fun createRootShell(globalMnt: Boolean = false): Shell {
|
||||
Log.w(TAG, "ksu failed: ", e)
|
||||
try {
|
||||
if (globalMnt) {
|
||||
builder.build("su")
|
||||
} else {
|
||||
builder.build("su", "-mm")
|
||||
} else {
|
||||
builder.build("su")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "su failed: ", e)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -25,18 +22,78 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object ModuleModify {
|
||||
suspend fun showRestoreConfirmation(context: Context): Boolean {
|
||||
val result = CompletableDeferred<Boolean>()
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.restore_confirm_title))
|
||||
.setMessage(context.getString(R.string.restore_confirm_message))
|
||||
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) }
|
||||
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
|
||||
.setOnCancelListener { result.complete(false) }
|
||||
.show()
|
||||
@Composable
|
||||
fun RestoreConfirmationDialog(
|
||||
showDialog: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = context.getString(R.string.restore_confirm_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = context.getString(R.string.restore_confirm_message),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(context.getString(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(context.getString(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AllowlistRestoreConfirmationDialog(
|
||||
showDialog: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
if (showDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = context.getString(R.string.allowlist_restore_confirm_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = context.getString(R.string.allowlist_restore_confirm_message),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(context.getString(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(context.getString(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return result.await()
|
||||
}
|
||||
|
||||
suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
|
||||
@@ -82,8 +139,19 @@ object ModuleModify {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
|
||||
val userConfirmed = showRestoreConfirmation(context)
|
||||
suspend fun restoreModules(
|
||||
context: Context,
|
||||
snackBarHost: SnackbarHostState,
|
||||
uri: Uri,
|
||||
showConfirmDialog: (Boolean) -> Unit,
|
||||
confirmResult: CompletableDeferred<Boolean>
|
||||
) {
|
||||
// 显示确认对话框
|
||||
withContext(Dispatchers.Main) {
|
||||
showConfirmDialog(true)
|
||||
}
|
||||
|
||||
val userConfirmed = confirmResult.await()
|
||||
if (!userConfirmed) return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -132,20 +200,6 @@ object ModuleModify {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showAllowlistRestoreConfirmation(context: Context): Boolean {
|
||||
val result = CompletableDeferred<Boolean>()
|
||||
withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(context.getString(R.string.allowlist_restore_confirm_title))
|
||||
.setMessage(context.getString(R.string.allowlist_restore_confirm_message))
|
||||
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) }
|
||||
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
|
||||
.setOnCancelListener { result.complete(false) }
|
||||
.show()
|
||||
}
|
||||
return result.await()
|
||||
}
|
||||
|
||||
suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -182,8 +236,19 @@ object ModuleModify {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
|
||||
val userConfirmed = showAllowlistRestoreConfirmation(context)
|
||||
suspend fun restoreAllowlist(
|
||||
context: Context,
|
||||
snackBarHost: SnackbarHostState,
|
||||
uri: Uri,
|
||||
showConfirmDialog: (Boolean) -> Unit,
|
||||
confirmResult: CompletableDeferred<Boolean>
|
||||
) {
|
||||
// 显示确认对话框
|
||||
withContext(Dispatchers.Main) {
|
||||
showConfirmDialog(true)
|
||||
}
|
||||
|
||||
val userConfirmed = confirmResult.await()
|
||||
if (!userConfirmed) return
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -246,13 +311,42 @@ object ModuleModify {
|
||||
context: Context,
|
||||
snackBarHost: SnackbarHostState,
|
||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
||||
) = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
scope.launch {
|
||||
restoreModules(context, snackBarHost, uri)
|
||||
): androidx.activity.result.ActivityResultLauncher<Intent> {
|
||||
var showRestoreDialog by remember { mutableStateOf(false) }
|
||||
var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
|
||||
var pendingUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
// 显示恢复确认对话框
|
||||
RestoreConfirmationDialog(
|
||||
showDialog = showRestoreDialog,
|
||||
onConfirm = {
|
||||
showRestoreDialog = false
|
||||
restoreConfirmResult?.complete(true)
|
||||
},
|
||||
onDismiss = {
|
||||
showRestoreDialog = false
|
||||
restoreConfirmResult?.complete(false)
|
||||
}
|
||||
)
|
||||
|
||||
return rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
pendingUri = uri
|
||||
scope.launch {
|
||||
val confirmResult = CompletableDeferred<Boolean>()
|
||||
restoreConfirmResult = confirmResult
|
||||
|
||||
restoreModules(
|
||||
context = context,
|
||||
snackBarHost = snackBarHost,
|
||||
uri = uri,
|
||||
showConfirmDialog = { show -> showRestoreDialog = show },
|
||||
confirmResult = confirmResult
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,13 +374,42 @@ object ModuleModify {
|
||||
context: Context,
|
||||
snackBarHost: SnackbarHostState,
|
||||
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
|
||||
) = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
scope.launch {
|
||||
restoreAllowlist(context, snackBarHost, uri)
|
||||
): androidx.activity.result.ActivityResultLauncher<Intent> {
|
||||
var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
|
||||
var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
|
||||
var pendingUri by remember { mutableStateOf<Uri?>(null) }
|
||||
|
||||
// 显示允许列表恢复确认对话框
|
||||
AllowlistRestoreConfirmationDialog(
|
||||
showDialog = showAllowlistRestoreDialog,
|
||||
onConfirm = {
|
||||
showAllowlistRestoreDialog = false
|
||||
allowlistRestoreConfirmResult?.complete(true)
|
||||
},
|
||||
onDismiss = {
|
||||
showAllowlistRestoreDialog = false
|
||||
allowlistRestoreConfirmResult?.complete(false)
|
||||
}
|
||||
)
|
||||
|
||||
return rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
pendingUri = uri
|
||||
scope.launch {
|
||||
val confirmResult = CompletableDeferred<Boolean>()
|
||||
allowlistRestoreConfirmResult = confirmResult
|
||||
|
||||
restoreAllowlist(
|
||||
context = context,
|
||||
snackBarHost = snackBarHost,
|
||||
uri = uri,
|
||||
showConfirmDialog = { show -> showAllowlistRestoreDialog = show },
|
||||
confirmResult = confirmResult
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipInputStream
|
||||
import com.sukisu.ultra.R
|
||||
import android.util.Log
|
||||
import java.io.IOException
|
||||
|
||||
object ModuleUtils {
|
||||
private const val TAG = "ModuleUtils"
|
||||
|
||||
fun extractModuleName(context: Context, uri: Uri): String {
|
||||
if (uri == Uri.EMPTY) {
|
||||
Log.e(TAG, "The supplied URI is empty")
|
||||
return context.getString(R.string.unknown_module)
|
||||
}
|
||||
|
||||
return try {
|
||||
Log.d(TAG, "Start extracting module names from URIs: $uri")
|
||||
|
||||
// 从URI路径中提取文件名
|
||||
val fileName = uri.lastPathSegment?.let { path ->
|
||||
val lastSlash = path.lastIndexOf('/')
|
||||
if (lastSlash != -1 && lastSlash < path.length - 1) {
|
||||
path.substring(lastSlash + 1)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module)
|
||||
|
||||
var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
|
||||
var moduleName = formattedFileName
|
||||
|
||||
try {
|
||||
// 打开ZIP文件输入流
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
if (inputStream == null) {
|
||||
Log.e(TAG, "Unable to get input stream from URI: $uri")
|
||||
return formattedFileName
|
||||
}
|
||||
|
||||
val zipInputStream = ZipInputStream(inputStream)
|
||||
var entry = zipInputStream.nextEntry
|
||||
|
||||
// 遍历ZIP文件中的条目,查找module.prop文件
|
||||
while (entry != null) {
|
||||
if (entry.name == "module.prop") {
|
||||
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
|
||||
var line: String?
|
||||
var nameFound = false
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line?.startsWith("name=") == true) {
|
||||
moduleName = line.substringAfter("=")
|
||||
moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
|
||||
nameFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
entry = zipInputStream.nextEntry
|
||||
}
|
||||
zipInputStream.close()
|
||||
Log.d(TAG, "Successfully extracted module name: $moduleName")
|
||||
moduleName
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error reading ZIP file: ${e.message}")
|
||||
formattedFileName
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception when extracting module name: ${e.message}")
|
||||
context.getString(R.string.unknown_module)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证URI是否有效并可访问
|
||||
fun isUriAccessible(context: Context, uri: Uri): Boolean {
|
||||
if (uri == Uri.EMPTY) return false
|
||||
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
inputStream?.close()
|
||||
inputStream != null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "The URI is inaccessible: $uri, Error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取URI的持久权限
|
||||
fun takePersistableUriPermission(context: Context, uri: Uri) {
|
||||
try {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
Log.d(TAG, "Persistent permissions for URIs have been obtained: $uri")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
|
||||
/**
|
||||
* 重启应用程序
|
||||
**/
|
||||
|
||||
fun Context.restartApp(
|
||||
activityClass: Class<out Activity>,
|
||||
finishCurrent: Boolean = true,
|
||||
clearTask: Boolean = true,
|
||||
newTask: Boolean = true
|
||||
) {
|
||||
val intent = Intent(this, activityClass)
|
||||
if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
|
||||
if (finishCurrent && this is Activity) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新启动器图标
|
||||
*/
|
||||
fun toggleLauncherIcon(context: Context, useAlt: Boolean) {
|
||||
val pm = context.packageManager
|
||||
val main = ComponentName(context, MainActivity::class.java.name)
|
||||
val alt = ComponentName(context, "${MainActivity::class.java.name}Alias")
|
||||
if (useAlt) {
|
||||
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
|
||||
} else {
|
||||
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
|
||||
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import android.content.Context
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Composable
|
||||
fun getSELinuxStatus(): String {
|
||||
fun getSELinuxStatus(context: Context): String {
|
||||
val shell = Shell.Builder.create().build("sh")
|
||||
val list = ArrayList<String>()
|
||||
|
||||
@@ -18,16 +16,16 @@ fun getSELinuxStatus(): String {
|
||||
|
||||
return if (result.isSuccess) {
|
||||
when (output) {
|
||||
"Enforcing" -> stringResource(R.string.selinux_status_enforcing)
|
||||
"Permissive" -> stringResource(R.string.selinux_status_permissive)
|
||||
"Disabled" -> stringResource(R.string.selinux_status_disabled)
|
||||
else -> stringResource(R.string.selinux_status_unknown)
|
||||
"Enforcing" -> context.getString(R.string.selinux_status_enforcing)
|
||||
"Permissive" -> context.getString(R.string.selinux_status_permissive)
|
||||
"Disabled" -> context.getString(R.string.selinux_status_disabled)
|
||||
else -> context.getString(R.string.selinux_status_unknown)
|
||||
}
|
||||
} else {
|
||||
if (output.contains("Permission denied")) {
|
||||
stringResource(R.string.selinux_status_enforcing)
|
||||
context.getString(R.string.selinux_status_enforcing)
|
||||
} else {
|
||||
stringResource(R.string.selinux_status_unknown)
|
||||
context.getString(R.string.selinux_status_unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
1093
manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt
Normal file
1093
manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,509 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
/**
|
||||
* Magisk模块脚本生成器
|
||||
* 用于生成各种启动脚本的内容
|
||||
*/
|
||||
object ScriptGenerator {
|
||||
|
||||
// 常量定义
|
||||
private const val DEFAULT_UNAME = "default"
|
||||
private const val DEFAULT_BUILD_TIME = "default"
|
||||
private const val LOG_DIR = "/data/adb/ksu/log"
|
||||
|
||||
/**
|
||||
* 生成所有脚本文件
|
||||
*/
|
||||
fun generateAllScripts(config: SuSFSManager.ModuleConfig): Map<String, String> {
|
||||
return mapOf(
|
||||
"service.sh" to generateServiceScript(config),
|
||||
"post-fs-data.sh" to generatePostFsDataScript(config),
|
||||
"post-mount.sh" to generatePostMountScript(config),
|
||||
"boot-completed.sh" to generateBootCompletedScript(config)
|
||||
)
|
||||
}
|
||||
|
||||
// 日志相关的通用脚本片段
|
||||
private fun generateLogSetup(logFileName: String): String = """
|
||||
# 日志目录
|
||||
LOG_DIR="$LOG_DIR"
|
||||
LOG_FILE="${'$'}LOG_DIR/$logFileName"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "${'$'}LOG_DIR"
|
||||
|
||||
# 获取当前时间
|
||||
get_current_time() {
|
||||
date '+%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// 二进制文件检查的通用脚本片段
|
||||
private fun generateBinaryCheck(targetPath: String): String = """
|
||||
# 检查SuSFS二进制文件
|
||||
SUSFS_BIN="$targetPath"
|
||||
if [ ! -f "${'$'}SUSFS_BIN" ]; then
|
||||
echo "$(get_current_time): SuSFS二进制文件未找到: ${'$'}SUSFS_BIN" >> "${'$'}LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
""".trimIndent()
|
||||
|
||||
/**
|
||||
* 生成service.sh脚本内容
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
private fun generateServiceScript(config: SuSFSManager.ModuleConfig): String {
|
||||
return buildString {
|
||||
appendLine("#!/system/bin/sh")
|
||||
appendLine("# SuSFS Service Script")
|
||||
appendLine("# 在系统服务启动后执行")
|
||||
appendLine()
|
||||
appendLine(generateLogSetup("susfs_service.log"))
|
||||
appendLine()
|
||||
appendLine(generateBinaryCheck(config.targetPath))
|
||||
appendLine()
|
||||
|
||||
if (shouldConfigureInService(config)) {
|
||||
// 添加SUS路径 (仅在不支持隐藏挂载时)
|
||||
if (!config.support158 && config.susPaths.isNotEmpty()) {
|
||||
appendLine()
|
||||
appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done")
|
||||
appendLine("sleep 45")
|
||||
generateSusPathsSection(config.susPaths)
|
||||
}
|
||||
|
||||
// 设置uname和构建时间
|
||||
generateUnameSection(config)
|
||||
|
||||
// 添加Kstat配置
|
||||
generateKstatSection(config.kstatConfigs, config.addKstatPaths)
|
||||
}
|
||||
|
||||
// 添加日志设置
|
||||
generateLogSettingSection(config.enableLog)
|
||||
|
||||
// 隐藏BL相关配置
|
||||
if (config.enableHideBl) {
|
||||
generateHideBlSection()
|
||||
}
|
||||
|
||||
// 清理工具残留
|
||||
if (config.enableCleanupResidue) {
|
||||
generateCleanupResidueSection()
|
||||
}
|
||||
|
||||
appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要在service中配置
|
||||
*/
|
||||
private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean {
|
||||
return config.susPaths.isNotEmpty() ||
|
||||
config.kstatConfigs.isNotEmpty() ||
|
||||
config.addKstatPaths.isNotEmpty() ||
|
||||
(!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME))
|
||||
}
|
||||
|
||||
private fun StringBuilder.generateLogSettingSection(enableLog: Boolean) {
|
||||
appendLine("# 设置日志启用状态")
|
||||
val logValue = if (enableLog) 1 else 0
|
||||
appendLine("\"${'$'}SUSFS_BIN\" enable_log $logValue")
|
||||
appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
private fun StringBuilder.generateSusPathsSection(susPaths: Set<String>) {
|
||||
if (susPaths.isNotEmpty()) {
|
||||
appendLine("# 添加SUS路径")
|
||||
susPaths.forEach { path ->
|
||||
appendLine("\"${'$'}SUSFS_BIN\" add_sus_path '$path'")
|
||||
appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SdCardPath")
|
||||
private fun StringBuilder.generateKstatSection(
|
||||
kstatConfigs: Set<String>,
|
||||
addKstatPaths: Set<String>
|
||||
) {
|
||||
// 添加Kstat路径
|
||||
if (addKstatPaths.isNotEmpty()) {
|
||||
appendLine("# 添加Kstat路径")
|
||||
addKstatPaths.forEach { path ->
|
||||
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat '$path'")
|
||||
appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
|
||||
// 添加Kstat静态配置
|
||||
if (kstatConfigs.isNotEmpty()) {
|
||||
appendLine("# 添加Kstat静态配置")
|
||||
kstatConfigs.forEach { config ->
|
||||
val parts = config.split("|")
|
||||
if (parts.size >= 13) {
|
||||
val path = parts[0]
|
||||
val params = parts.drop(1).joinToString("' '", "'", "'")
|
||||
appendLine()
|
||||
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat_statically '$path' $params")
|
||||
appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
appendLine("\"${'$'}SUSFS_BIN\" update_sus_kstat '$path'")
|
||||
appendLine("echo \"$(get_current_time): 更新Kstat配置: $path\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.generateUnameSection(config: SuSFSManager.ModuleConfig) {
|
||||
if (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) {
|
||||
appendLine("# 设置uname和构建时间")
|
||||
appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'")
|
||||
appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.generateHideBlSection() {
|
||||
appendLine("# 隐藏BL 来自 Shamiko 脚本")
|
||||
appendLine(
|
||||
"""
|
||||
RESETPROP_BIN="/data/adb/ksu/bin/resetprop"
|
||||
|
||||
check_reset_prop() {
|
||||
local NAME=$1
|
||||
local EXPECTED=$2
|
||||
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
|
||||
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
|
||||
}
|
||||
|
||||
check_missing_prop() {
|
||||
local NAME=$1
|
||||
local EXPECTED=$2
|
||||
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
|
||||
[ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
|
||||
}
|
||||
|
||||
check_missing_match_prop() {
|
||||
local NAME=$1
|
||||
local EXPECTED=$2
|
||||
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
|
||||
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
|
||||
[ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
|
||||
}
|
||||
|
||||
contains_reset_prop() {
|
||||
local NAME=$1
|
||||
local CONTAINS=$2
|
||||
local NEWVAL=$3
|
||||
case "$("${'$'}RESETPROP_BIN" ${'$'}NAME)" in
|
||||
*"${'$'}CONTAINS"*) "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}NEWVAL ;;
|
||||
esac
|
||||
}
|
||||
""".trimIndent())
|
||||
appendLine()
|
||||
appendLine("sleep 30")
|
||||
appendLine()
|
||||
appendLine("\"${'$'}RESETPROP_BIN\" -w sys.boot_completed 0")
|
||||
|
||||
// 添加所有系统属性重置
|
||||
val systemProps = listOf(
|
||||
"ro.boot.vbmeta.invalidate_on_error" to "yes",
|
||||
"ro.boot.vbmeta.avb_version" to "1.2",
|
||||
"ro.boot.vbmeta.hash_alg" to "sha256",
|
||||
"ro.boot.vbmeta.size" to "19968",
|
||||
"ro.boot.vbmeta.device_state" to "locked",
|
||||
"ro.boot.verifiedbootstate" to "green",
|
||||
"ro.boot.flash.locked" to "1",
|
||||
"ro.boot.veritymode" to "enforcing",
|
||||
"ro.boot.warranty_bit" to "0",
|
||||
"ro.warranty_bit" to "0",
|
||||
"ro.debuggable" to "0",
|
||||
"ro.force.debuggable" to "0",
|
||||
"ro.secure" to "1",
|
||||
"ro.adb.secure" to "1",
|
||||
"ro.build.type" to "user",
|
||||
"ro.build.tags" to "release-keys",
|
||||
"ro.vendor.boot.warranty_bit" to "0",
|
||||
"ro.vendor.warranty_bit" to "0",
|
||||
"vendor.boot.vbmeta.device_state" to "locked",
|
||||
"vendor.boot.verifiedbootstate" to "green",
|
||||
"sys.oem_unlock_allowed" to "0",
|
||||
"ro.secureboot.lockstate" to "locked",
|
||||
"ro.boot.realmebootstate" to "green",
|
||||
"ro.boot.realme.lockstate" to "1",
|
||||
"ro.crypto.state" to "encrypted"
|
||||
)
|
||||
|
||||
systemProps.forEach { (prop, value) ->
|
||||
when {
|
||||
prop.startsWith("ro.boot.vbmeta") && prop.endsWith("_on_error") ->
|
||||
appendLine("check_missing_prop \"$prop\" \"$value\"")
|
||||
prop.contains("device_state") || prop.contains("verifiedbootstate") ->
|
||||
appendLine("check_missing_match_prop \"$prop\" \"$value\"")
|
||||
else ->
|
||||
appendLine("check_reset_prop \"$prop\" \"$value\"")
|
||||
}
|
||||
}
|
||||
|
||||
appendLine()
|
||||
appendLine("# Hide adb debugging traces")
|
||||
appendLine("resetprop \"sys.usb.adb.disabled\" \" \"")
|
||||
appendLine()
|
||||
|
||||
appendLine("# Hide recovery boot mode")
|
||||
appendLine("contains_reset_prop \"ro.bootmode\" \"recovery\" \"unknown\"")
|
||||
appendLine("contains_reset_prop \"ro.boot.bootmode\" \"recovery\" \"unknown\"")
|
||||
appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"")
|
||||
appendLine()
|
||||
|
||||
appendLine("# Hide cloudphone detection")
|
||||
appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
// 清理残留脚本生成
|
||||
private fun StringBuilder.generateCleanupResidueSection() {
|
||||
appendLine("# 清理工具残留文件")
|
||||
appendLine("echo \"$(get_current_time): 开始清理工具残留\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
|
||||
// 定义清理函数
|
||||
appendLine("""
|
||||
cleanup_path() {
|
||||
local path="$1"
|
||||
local desc="$2"
|
||||
local current="$3"
|
||||
local total="$4"
|
||||
|
||||
if [ -n "${'$'}desc" ]; then
|
||||
echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path (${'$'}desc)" >> "${'$'}LOG_FILE"
|
||||
else
|
||||
echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path" >> "${'$'}LOG_FILE"
|
||||
fi
|
||||
|
||||
if rm -rf "${'$'}path" 2>/dev/null; then
|
||||
echo "$(get_current_time): ✓ 成功清理: ${'$'}path" >> "${'$'}LOG_FILE"
|
||||
else
|
||||
echo "$(get_current_time): ✗ 清理失败或不存在: ${'$'}path" >> "${'$'}LOG_FILE"
|
||||
fi
|
||||
}
|
||||
""".trimIndent())
|
||||
|
||||
appendLine()
|
||||
appendLine("# 开始清理各种工具残留")
|
||||
appendLine("TOTAL=33")
|
||||
appendLine()
|
||||
|
||||
val cleanupPaths = listOf(
|
||||
"/data/local/stryker/" to "Stryker残留",
|
||||
"/data/system/AppRetention" to "AppRetention残留",
|
||||
"/data/local/tmp/luckys" to "Luck Tool残留",
|
||||
"/data/local/tmp/HyperCeiler" to "西米露残留",
|
||||
"/data/local/tmp/simpleHook" to "simple Hook残留",
|
||||
"/data/local/tmp/DisabledAllGoogleServices" to "谷歌省电模块残留",
|
||||
"/data/local/MIO" to "解包软件",
|
||||
"/data/DNA" to "解包软件",
|
||||
"/data/local/tmp/cleaner_starter" to "质感清理残留",
|
||||
"/data/local/tmp/byyang" to "",
|
||||
"/data/local/tmp/mount_mask" to "",
|
||||
"/data/local/tmp/mount_mark" to "",
|
||||
"/data/local/tmp/scriptTMP" to "",
|
||||
"/data/local/luckys" to "",
|
||||
"/data/local/tmp/horae_control.log" to "",
|
||||
"/data/gpu_freq_table.conf" to "",
|
||||
"/storage/emulated/0/Download/advanced/" to "",
|
||||
"/storage/emulated/0/Documents/advanced/" to "爱玩机",
|
||||
"/storage/emulated/0/Android/naki/" to "旧版asoulopt",
|
||||
"/data/swap_config.conf" to "scene附加模块2",
|
||||
"/data/local/tmp/resetprop" to "",
|
||||
"/dev/cpuset/AppOpt/" to "AppOpt模块",
|
||||
"/storage/emulated/0/Android/Clash/" to "Clash for Magisk模块",
|
||||
"/storage/emulated/0/Android/Yume-Yunyun/" to "网易云后台优化模块",
|
||||
"/data/local/tmp/Surfing_update" to "Surfing模块缓存",
|
||||
"/data/encore/custom_default_cpu_gov" to "encore模块",
|
||||
"/data/encore/default_cpu_gov" to "encore模块",
|
||||
"/data/local/tmp/yshell" to "",
|
||||
"/data/local/tmp/encore_logo.png" to "",
|
||||
"/storage/emulated/legacy/" to "",
|
||||
"/storage/emulated/elgg/" to "",
|
||||
"/data/system/junge/" to "",
|
||||
"/data/local/tmp/mount_namespace" to "挂载命名空间残留"
|
||||
)
|
||||
|
||||
cleanupPaths.forEachIndexed { index, (path, desc) ->
|
||||
val current = index + 1
|
||||
appendLine("cleanup_path '$path' '$desc' $current \$TOTAL")
|
||||
}
|
||||
|
||||
appendLine()
|
||||
appendLine("echo \"$(get_current_time): 工具残留清理完成\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成post-fs-data.sh脚本内容
|
||||
*/
|
||||
private fun generatePostFsDataScript(config: SuSFSManager.ModuleConfig): String {
|
||||
return buildString {
|
||||
appendLine("#!/system/bin/sh")
|
||||
appendLine("# SuSFS Post-FS-Data Script")
|
||||
appendLine("# 在文件系统挂载后但在系统完全启动前执行")
|
||||
appendLine()
|
||||
appendLine(generateLogSetup("susfs_post_fs_data.log"))
|
||||
appendLine()
|
||||
appendLine(generateBinaryCheck(config.targetPath))
|
||||
appendLine()
|
||||
appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
|
||||
// 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行
|
||||
if (config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) {
|
||||
appendLine("# 设置uname和构建时间")
|
||||
appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'")
|
||||
appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
generateUmountZygoteIsoServiceSection(config.umountForZygoteIsoService, config.support158)
|
||||
|
||||
appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新的生成方法
|
||||
private fun StringBuilder.generateUmountZygoteIsoServiceSection(umountForZygoteIsoService: Boolean, support158: Boolean) {
|
||||
if (support158) {
|
||||
appendLine("# 设置Zygote隔离服务卸载状态")
|
||||
val umountValue = if (umountForZygoteIsoService) 1 else 0
|
||||
appendLine("\"${'$'}SUSFS_BIN\" umount_for_zygote_iso_service $umountValue")
|
||||
appendLine("echo \"$(get_current_time): Zygote隔离服务卸载设置为: ${if (umountForZygoteIsoService) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成post-mount.sh脚本内容
|
||||
*/
|
||||
private fun generatePostMountScript(config: SuSFSManager.ModuleConfig): String {
|
||||
return buildString {
|
||||
appendLine("#!/system/bin/sh")
|
||||
appendLine("# SuSFS Post-Mount Script")
|
||||
appendLine("# 在所有分区挂载完成后执行")
|
||||
appendLine()
|
||||
appendLine(generateLogSetup("susfs_post_mount.log"))
|
||||
appendLine()
|
||||
appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
appendLine(generateBinaryCheck(config.targetPath))
|
||||
appendLine()
|
||||
|
||||
// 添加SUS挂载
|
||||
if (config.susMounts.isNotEmpty()) {
|
||||
appendLine("# 添加SUS挂载")
|
||||
config.susMounts.forEach { mount ->
|
||||
appendLine("\"${'$'}SUSFS_BIN\" add_sus_mount '$mount'")
|
||||
appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
|
||||
// 添加尝试卸载
|
||||
if (config.tryUmounts.isNotEmpty()) {
|
||||
appendLine("# 添加尝试卸载")
|
||||
config.tryUmounts.forEach { umount ->
|
||||
val parts = umount.split("|")
|
||||
if (parts.size == 2) {
|
||||
val path = parts[0]
|
||||
val mode = parts[1]
|
||||
appendLine("\"${'$'}SUSFS_BIN\" add_try_umount '$path' $mode")
|
||||
appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
appendLine()
|
||||
}
|
||||
|
||||
appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成boot-completed.sh脚本内容
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
private fun generateBootCompletedScript(config: SuSFSManager.ModuleConfig): String {
|
||||
return buildString {
|
||||
appendLine("#!/system/bin/sh")
|
||||
appendLine("# SuSFS Boot-Completed Script")
|
||||
appendLine("# 在系统完全启动后执行")
|
||||
appendLine()
|
||||
appendLine(generateLogSetup("susfs_boot_completed.log"))
|
||||
appendLine()
|
||||
appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
appendLine(generateBinaryCheck(config.targetPath))
|
||||
appendLine()
|
||||
|
||||
// 仅在支持隐藏挂载功能时执行相关配置
|
||||
if (config.support158) {
|
||||
// SUS挂载隐藏控制
|
||||
val hideValue = if (config.hideSusMountsForAllProcs) 1 else 0
|
||||
appendLine("# 设置SUS挂载隐藏控制")
|
||||
appendLine("\"${'$'}SUSFS_BIN\" hide_sus_mnts_for_all_procs $hideValue")
|
||||
appendLine("echo \"$(get_current_time): SUS挂载隐藏控制设置为: ${if (config.hideSusMountsForAllProcs) "对所有进程隐藏" else "仅对非KSU进程隐藏"}\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
|
||||
// 路径设置和SUS路径设置
|
||||
if (config.susPaths.isNotEmpty()) {
|
||||
generatePathSettingSection(config.androidDataPath, config.sdcardPath)
|
||||
appendLine()
|
||||
appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done")
|
||||
appendLine("sleep 45")
|
||||
appendLine()
|
||||
generateSusPathsSection(config.susPaths)
|
||||
}
|
||||
}
|
||||
|
||||
appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SdCardPath")
|
||||
private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) {
|
||||
appendLine("# 路径配置")
|
||||
appendLine("# 设置Android Data路径")
|
||||
appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done")
|
||||
appendLine("\"${'$'}SUSFS_BIN\" set_android_data_root_path '$androidDataPath'")
|
||||
appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
appendLine("# 设置SD卡路径")
|
||||
appendLine("\"${'$'}SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'")
|
||||
appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"${'$'}LOG_FILE\"")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成module.prop文件内容
|
||||
*/
|
||||
fun generateModuleProp(moduleId: String): String {
|
||||
val moduleVersion = "v1.0.2"
|
||||
val moduleVersionCode = "1002"
|
||||
|
||||
return """
|
||||
id=$moduleId
|
||||
name=SuSFS Manager
|
||||
version=$moduleVersion
|
||||
versionCode=$moduleVersionCode
|
||||
author=ShirkNeko
|
||||
description=SuSFS Manager Auto Configuration Module (自动生成请不要手动卸载或删除该模块! / Automatically generated Please do not manually uninstall or delete the module!)
|
||||
updateJson=
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dergoogler.mmrl.platform.Platform.Companion.context
|
||||
import com.google.gson.Gson
|
||||
import com.sukisu.ultra.KernelVersion
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.getKernelVersion
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.core.content.edit
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "HomeViewModel"
|
||||
private const val PREFS_NAME = "home_cache"
|
||||
private const val KEY_SYSTEM_STATUS = "system_status"
|
||||
private const val KEY_SYSTEM_INFO = "system_info"
|
||||
private const val KEY_VERSION_INFO = "version_info"
|
||||
private const val KEY_LAST_UPDATE = "last_update_time"
|
||||
}
|
||||
|
||||
// 系统状态
|
||||
data class SystemStatus(
|
||||
val isManager: Boolean = false,
|
||||
val ksuVersion: Int? = null,
|
||||
val ksuFullVersion : String? = null,
|
||||
val lkmMode: Boolean? = null,
|
||||
val kernelVersion: KernelVersion = getKernelVersion(),
|
||||
val isRootAvailable: Boolean = false,
|
||||
val isKpmConfigured: Boolean = false,
|
||||
val requireNewKernel: Boolean = false
|
||||
)
|
||||
|
||||
// 系统信息
|
||||
data class SystemInfo(
|
||||
val kernelRelease: String = "",
|
||||
val androidVersion: String = "",
|
||||
val deviceModel: String = "",
|
||||
val managerVersion: Pair<String, Long> = Pair("", 0L),
|
||||
val seLinuxStatus: String = "",
|
||||
val kpmVersion: String = "",
|
||||
val suSFSStatus: String = "",
|
||||
val suSFSVersion: String = "",
|
||||
val suSFSVariant: String = "",
|
||||
val suSFSFeatures: String = "",
|
||||
val susSUMode: String = "",
|
||||
val superuserCount: Int = 0,
|
||||
val moduleCount: Int = 0,
|
||||
val kpmModuleCount: Int = 0
|
||||
)
|
||||
|
||||
private val gson = Gson()
|
||||
private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
|
||||
|
||||
var systemStatus by mutableStateOf(SystemStatus())
|
||||
private set
|
||||
|
||||
var systemInfo by mutableStateOf(SystemInfo())
|
||||
private set
|
||||
|
||||
var latestVersionInfo by mutableStateOf(LatestVersionInfo())
|
||||
private set
|
||||
|
||||
var isSimpleMode by mutableStateOf(false)
|
||||
private set
|
||||
var isKernelSimpleMode by mutableStateOf(false)
|
||||
private set
|
||||
var isHideVersion by mutableStateOf(false)
|
||||
private set
|
||||
var isHideOtherInfo by mutableStateOf(false)
|
||||
private set
|
||||
var isHideSusfsStatus by mutableStateOf(false)
|
||||
private set
|
||||
var isHideLinkCard by mutableStateOf(false)
|
||||
private set
|
||||
var showKpmInfo by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun loadUserSettings(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
isSimpleMode = prefs.getBoolean("is_simple_mode", false)
|
||||
isKernelSimpleMode = prefs.getBoolean("is_kernel_simple_mode", false)
|
||||
isHideVersion = prefs.getBoolean("is_hide_version", false)
|
||||
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false)
|
||||
isHideSusfsStatus = prefs.getBoolean("is_hide_susfs_status", false)
|
||||
isHideLinkCard = prefs.getBoolean("is_hide_link_card", false)
|
||||
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeData() {
|
||||
viewModelScope.launch {
|
||||
loadCachedData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCachedData() {
|
||||
prefs.getString(KEY_SYSTEM_STATUS, null)?.let {
|
||||
systemStatus = gson.fromJson(it, SystemStatus::class.java)
|
||||
}
|
||||
prefs.getString(KEY_SYSTEM_INFO, null)?.let {
|
||||
systemInfo = gson.fromJson(it, SystemInfo::class.java)
|
||||
}
|
||||
prefs.getString(KEY_VERSION_INFO, null)?.let {
|
||||
latestVersionInfo = gson.fromJson(it, LatestVersionInfo::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndSaveData() {
|
||||
fetchSystemStatus()
|
||||
fetchSystemInfo()
|
||||
withContext(Dispatchers.IO) {
|
||||
prefs.edit {
|
||||
putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus))
|
||||
putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo))
|
||||
putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo))
|
||||
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates(context: Context) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("check_update", true)
|
||||
|
||||
if (checkUpdate) {
|
||||
val newVersionInfo = checkNewVersion()
|
||||
latestVersionInfo = newVersionInfo
|
||||
prefs.edit {
|
||||
putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo))
|
||||
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking for updates", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshAllData(context: Context) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
fetchAndSaveData()
|
||||
checkForUpdates(context)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error refreshing data", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSystemStatus() {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val kernelVersion = getKernelVersion()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val fullVersion = Natives.getFullVersion()
|
||||
val ksuFullVersion = if (isKernelSimpleMode) {
|
||||
val startIndex = fullVersion.indexOf('v')
|
||||
if (startIndex >= 0) {
|
||||
val endIndex = fullVersion.indexOf('-', startIndex)
|
||||
val versionStr = if (endIndex > startIndex) {
|
||||
fullVersion.substring(startIndex, endIndex)
|
||||
} else {
|
||||
fullVersion.substring(startIndex)
|
||||
}
|
||||
val numericVersion = "v" + (Regex("""\d+(\.\d+)*""").find(versionStr)?.value ?: versionStr)
|
||||
numericVersion
|
||||
} else {
|
||||
fullVersion
|
||||
}
|
||||
} else {
|
||||
fullVersion
|
||||
}
|
||||
|
||||
val lkmMode = ksuVersion?.let {
|
||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
||||
}
|
||||
|
||||
systemStatus = SystemStatus(
|
||||
isManager = isManager,
|
||||
ksuVersion = ksuVersion,
|
||||
ksuFullVersion = ksuFullVersion,
|
||||
lkmMode = lkmMode,
|
||||
kernelVersion = kernelVersion,
|
||||
isRootAvailable = rootAvailable(),
|
||||
isKpmConfigured = Natives.isKPMEnabled(),
|
||||
requireNewKernel = isManager && Natives.requireNewKernel()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching system status", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private suspend fun fetchSystemInfo() {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val uname = Os.uname()
|
||||
val kpmVersion = getKpmVersion()
|
||||
val suSFS = getSuSFS()
|
||||
var suSFSVersion = ""
|
||||
var suSFSVariant = ""
|
||||
var suSFSFeatures = ""
|
||||
var susSUMode = ""
|
||||
|
||||
if (suSFS == "Supported") {
|
||||
suSFSVersion = getSuSFSVersion()
|
||||
if (suSFSVersion.isNotEmpty()) {
|
||||
suSFSVariant = getSuSFSVariant()
|
||||
suSFSFeatures = getSuSFSFeatures()
|
||||
val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
|
||||
if (isSUS_SU) {
|
||||
susSUMode = try {
|
||||
susfsSUS_SU_Mode().toString()
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
systemInfo = SystemInfo(
|
||||
kernelRelease = uname.release,
|
||||
androidVersion = Build.VERSION.RELEASE,
|
||||
deviceModel = getDeviceModel(),
|
||||
managerVersion = getManagerVersion(ksuApp.applicationContext),
|
||||
seLinuxStatus = getSELinuxStatus(context),
|
||||
kpmVersion = kpmVersion,
|
||||
suSFSStatus = suSFS,
|
||||
suSFSVersion = suSFSVersion,
|
||||
suSFSVariant = suSFSVariant,
|
||||
suSFSFeatures = suSFSFeatures,
|
||||
susSUMode = susSUMode,
|
||||
superuserCount = getSuperuserCount(),
|
||||
moduleCount = getModuleCount(),
|
||||
kpmModuleCount = getKpmModuleCount()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching system info", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): String {
|
||||
var manufacturer =
|
||||
Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)
|
||||
if (!Build.BRAND.equals(Build.MANUFACTURER, ignoreCase = true)) {
|
||||
manufacturer += " " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)
|
||||
}
|
||||
manufacturer += " " + Build.MODEL + " "
|
||||
return manufacturer
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getDeviceModel(): 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
|
||||
)
|
||||
var result = getDeviceInfo()
|
||||
for (key in marketNameKeys) {
|
||||
val marketName = getMethod.invoke(null, key, "") as String
|
||||
if (marketName.isNotEmpty()) {
|
||||
result = marketName
|
||||
break
|
||||
}
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting device model", e)
|
||||
getDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getManagerVersion(context: Context): Pair<String, Long> {
|
||||
return try {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
||||
val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||
Pair(packageInfo.versionName!!, versionCode)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting manager version", e)
|
||||
Pair("", 0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class KpmViewModel : ViewModel() {
|
||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||
private set
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -8,20 +9,67 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.dergoogler.mmrl.platform.model.ModuleConfig
|
||||
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.text.Collator
|
||||
import java.text.DecimalFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class ModuleViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ModuleViewModel"
|
||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
|
||||
}
|
||||
|
||||
// 模块大小缓存管理器
|
||||
private lateinit var moduleSizeCache: ModuleSizeCache
|
||||
|
||||
fun initializeCache(context: Context) {
|
||||
if (!::moduleSizeCache.isInitialized) {
|
||||
moduleSizeCache = ModuleSizeCache(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun getModuleSize(dirId: String): String {
|
||||
if (!::moduleSizeCache.isInitialized) {
|
||||
return "0 KB"
|
||||
}
|
||||
val size = moduleSizeCache.getModuleSize(dirId)
|
||||
return formatFileSize(size)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有模块的大小缓存
|
||||
* 只在安装、卸载、更新模块后调用
|
||||
*/
|
||||
fun refreshModuleSizeCache() {
|
||||
if (!::moduleSizeCache.isInitialized) return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
Log.d(TAG, "开始刷新模块大小缓存")
|
||||
val currentModules = modules.map { it.dirId }
|
||||
moduleSizeCache.refreshCache(currentModules)
|
||||
Log.d(TAG, "模块大小缓存刷新完成")
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleInfo(
|
||||
@@ -38,6 +86,7 @@ class ModuleViewModel : ViewModel() {
|
||||
val hasWebUi: Boolean,
|
||||
val hasActionScript: Boolean,
|
||||
val dirId: String, // real module id (dir name)
|
||||
var config: ModuleConfig? = null,
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
@@ -65,6 +114,8 @@ class ModuleViewModel : ViewModel() {
|
||||
|
||||
fun markNeedRefresh() {
|
||||
isNeedRefresh = true
|
||||
// 标记需要刷新时,同时刷新大小缓存
|
||||
refreshModuleSizeCache()
|
||||
}
|
||||
|
||||
fun fetchModuleList() {
|
||||
@@ -98,9 +149,49 @@ class ModuleViewModel : ViewModel() {
|
||||
obj.optString("updateJson"),
|
||||
obj.optBoolean("web"),
|
||||
obj.optBoolean("action"),
|
||||
obj.getString("dir_id"),
|
||||
obj.getString("dir_id")
|
||||
)
|
||||
}.toList()
|
||||
launch {
|
||||
modules.forEach { module ->
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
runCatching {
|
||||
module.config = module.id.asModuleConfig
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "Failed to load config from id for module ${module.id}", e)
|
||||
}
|
||||
if (module.config == null) {
|
||||
runCatching {
|
||||
module.config = module.name.asModuleConfig
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "Failed to load config from name for module ${module.id}", e)
|
||||
}
|
||||
}
|
||||
if (module.config == null) {
|
||||
runCatching {
|
||||
module.config = module.description.asModuleConfig
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "Failed to load config from description for module ${module.id}", e)
|
||||
}
|
||||
}
|
||||
if (module.config == null) {
|
||||
module.config = ModuleConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load any config for module ${module.id}", e)
|
||||
module.config = ModuleConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 首次加载模块列表时,初始化缓存
|
||||
if (::moduleSizeCache.isInitialized) {
|
||||
val currentModules = modules.map { it.dirId }
|
||||
moduleSizeCache.initializeCacheIfNeeded(currentModules)
|
||||
}
|
||||
|
||||
isNeedRefresh = false
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "fetchModuleList: ", e)
|
||||
@@ -117,6 +208,10 @@ class ModuleViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeVersionString(version: String): String {
|
||||
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
|
||||
}
|
||||
|
||||
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
||||
val empty = Triple("", "", "")
|
||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||
@@ -126,19 +221,32 @@ class ModuleViewModel : ViewModel() {
|
||||
val result = kotlin.runCatching {
|
||||
val url = m.updateJson
|
||||
Log.i(TAG, "checkUpdate url: $url")
|
||||
val response = okhttp3.OkHttpClient()
|
||||
.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
val client = okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
val request = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", CUSTOM_USER_AGENT)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
Log.d(TAG, "checkUpdate code: ${response.code}")
|
||||
if (response.isSuccessful) {
|
||||
response.body?.string() ?: ""
|
||||
} else {
|
||||
Log.d(TAG, "checkUpdate failed: ${response.message}")
|
||||
""
|
||||
}
|
||||
}.getOrDefault("")
|
||||
}.getOrElse { e ->
|
||||
Log.e(TAG, "checkUpdate exception", e)
|
||||
""
|
||||
}
|
||||
|
||||
Log.i(TAG, "checkUpdate result: $result")
|
||||
|
||||
if (result.isEmpty()) {
|
||||
@@ -149,7 +257,8 @@ class ModuleViewModel : ViewModel() {
|
||||
JSONObject(result)
|
||||
}.getOrNull() ?: return empty
|
||||
|
||||
val version = updateJson.optString("version", "")
|
||||
var version = updateJson.optString("version", "")
|
||||
version = sanitizeVersionString(version)
|
||||
val versionCode = updateJson.optInt("versionCode", 0)
|
||||
val zipUrl = updateJson.optString("zipUrl", "")
|
||||
val changelog = updateJson.optString("changelog", "")
|
||||
@@ -160,3 +269,171 @@ class ModuleViewModel : ViewModel() {
|
||||
return Triple(zipUrl, version, changelog)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块大小缓存管理器
|
||||
*/
|
||||
class ModuleSizeCache(context: Context) {
|
||||
companion object {
|
||||
private const val TAG = "ModuleSizeCache"
|
||||
private const val CACHE_PREFS_NAME = "module_size_cache"
|
||||
private const val CACHE_VERSION_KEY = "cache_version"
|
||||
private const val CACHE_INITIALIZED_KEY = "cache_initialized"
|
||||
private const val CURRENT_CACHE_VERSION = 1
|
||||
}
|
||||
|
||||
private val cachePrefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val sizeCache = mutableMapOf<String, Long>()
|
||||
|
||||
init {
|
||||
loadCacheFromPrefs()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载缓存
|
||||
*/
|
||||
private fun loadCacheFromPrefs() {
|
||||
try {
|
||||
val cacheVersion = cachePrefs.getInt(CACHE_VERSION_KEY, 0)
|
||||
if (cacheVersion != CURRENT_CACHE_VERSION) {
|
||||
Log.d(TAG, "缓存版本不匹配,清空缓存")
|
||||
clearCache()
|
||||
return
|
||||
}
|
||||
|
||||
val allEntries = cachePrefs.all
|
||||
for ((key, value) in allEntries) {
|
||||
if (key != CACHE_VERSION_KEY && key != CACHE_INITIALIZED_KEY && value is Long) {
|
||||
sizeCache[key] = value
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "从缓存加载了 ${sizeCache.size} 个模块大小数据")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "加载缓存失败", e)
|
||||
clearCache()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存缓存到SharedPreferences
|
||||
*/
|
||||
private fun saveCacheToPrefs() {
|
||||
try {
|
||||
cachePrefs.edit {
|
||||
putInt(CACHE_VERSION_KEY, CURRENT_CACHE_VERSION)
|
||||
putBoolean(CACHE_INITIALIZED_KEY, true)
|
||||
|
||||
for ((dirId, size) in sizeCache) {
|
||||
putLong(dirId, size)
|
||||
}
|
||||
|
||||
}
|
||||
Log.d(TAG, "保存了 ${sizeCache.size} 个模块大小到缓存")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "保存缓存失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块大小(从缓存)
|
||||
*/
|
||||
fun getModuleSize(dirId: String): Long {
|
||||
return sizeCache[dirId] ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否已初始化,如果没有则初始化
|
||||
*/
|
||||
fun initializeCacheIfNeeded(currentModules: List<String>) {
|
||||
val isInitialized = cachePrefs.getBoolean(CACHE_INITIALIZED_KEY, false)
|
||||
if (!isInitialized || sizeCache.isEmpty()) {
|
||||
Log.d(TAG, "首次初始化缓存,计算所有模块大小")
|
||||
refreshCache(currentModules)
|
||||
} else {
|
||||
// 检查是否有新模块需要计算大小
|
||||
val newModules = currentModules.filter { !sizeCache.containsKey(it) }
|
||||
if (newModules.isNotEmpty()) {
|
||||
Log.d(TAG, "发现 ${newModules.size} 个新模块,计算大小: $newModules")
|
||||
for (dirId in newModules) {
|
||||
val size = calculateModuleFolderSize(dirId)
|
||||
sizeCache[dirId] = size
|
||||
Log.d(TAG, "新模块 $dirId 大小: ${formatFileSize(size)}")
|
||||
}
|
||||
saveCacheToPrefs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新所有模块的大小缓存
|
||||
*/
|
||||
fun refreshCache(currentModules: List<String>) {
|
||||
try {
|
||||
// 清理不存在的模块缓存
|
||||
val toRemove = sizeCache.keys.filter { it !in currentModules }
|
||||
toRemove.forEach { sizeCache.remove(it) }
|
||||
|
||||
if (toRemove.isNotEmpty()) {
|
||||
Log.d(TAG, "清理了 ${toRemove.size} 个不存在的模块缓存: $toRemove")
|
||||
}
|
||||
|
||||
// 计算所有当前模块的大小
|
||||
for (dirId in currentModules) {
|
||||
val size = calculateModuleFolderSize(dirId)
|
||||
sizeCache[dirId] = size
|
||||
Log.d(TAG, "更新模块 $dirId 大小: ${formatFileSize(size)}")
|
||||
}
|
||||
|
||||
// 保存到持久化存储
|
||||
saveCacheToPrefs()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "刷新缓存失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
private fun clearCache() {
|
||||
sizeCache.clear()
|
||||
cachePrefs.edit { clear() }
|
||||
Log.d(TAG, "清空所有缓存")
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际计算模块文件夹大小
|
||||
*/
|
||||
private fun calculateModuleFolderSize(dirId: String): Long {
|
||||
return try {
|
||||
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "du -sb /data/adb/modules/$dirId"))
|
||||
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||
val output = reader.readLine()
|
||||
process.waitFor()
|
||||
reader.close()
|
||||
|
||||
if (output != null) {
|
||||
val sizeStr = output.split("\t").firstOrNull()
|
||||
sizeStr?.toLongOrNull() ?: 0L
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "计算模块大小失败 $dirId: ${e.message}")
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小的工具函数
|
||||
*/
|
||||
fun formatFileSize(bytes: Long): String {
|
||||
if (bytes <= 0) return "0 KB"
|
||||
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB")
|
||||
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
|
||||
|
||||
return DecimalFormat("#,##0.#").format(
|
||||
bytes / 1024.0.pow(digitGroups.toDouble())
|
||||
) + " " + units[digitGroups]
|
||||
}
|
||||
@@ -1,38 +1,91 @@
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.parcelize.Parcelize
|
||||
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
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||
import com.sukisu.ultra.ui.webui.getInstalledPackagesAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
|
||||
// 应用分类
|
||||
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
|
||||
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
|
||||
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
|
||||
CUSTOM(com.sukisu.ultra.R.string.category_custom_apps, "CUSTOM"),
|
||||
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
|
||||
|
||||
companion object {
|
||||
fun fromPersistKey(key: String): AppCategory {
|
||||
return entries.find { it.persistKey == key } ?: ALL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 排序方式
|
||||
enum class SortType(val displayNameRes: Int, val persistKey: String) {
|
||||
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
|
||||
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
|
||||
INSTALL_TIME_NEW(com.sukisu.ultra.R.string.sort_install_time_new, "INSTALL_TIME_NEW"),
|
||||
INSTALL_TIME_OLD(com.sukisu.ultra.R.string.sort_install_time_old, "INSTALL_TIME_OLD"),
|
||||
SIZE_DESC(com.sukisu.ultra.R.string.sort_size_desc, "SIZE_DESC"),
|
||||
SIZE_ASC(com.sukisu.ultra.R.string.sort_size_asc, "SIZE_ASC"),
|
||||
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
|
||||
|
||||
companion object {
|
||||
fun fromPersistKey(key: String): SortType {
|
||||
return entries.find { it.persistKey == key } ?: NAME_ASC
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class SuperUserViewModel : ViewModel() {
|
||||
val isPlatformAlive get() = Platform.isAlive
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SuperUserViewModel"
|
||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
private const val PREFS_NAME = "settings"
|
||||
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
|
||||
private const val KEY_SELECTED_CATEGORY = "selected_category"
|
||||
private const val KEY_CURRENT_SORT_TYPE = "current_sort_type"
|
||||
private const val CORE_POOL_SIZE = 4
|
||||
private const val MAX_POOL_SIZE = 8
|
||||
private const val KEEP_ALIVE_TIME = 60L
|
||||
private const val BATCH_SIZE = 20
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@@ -61,8 +114,35 @@ class SuperUserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private val appProcessingThreadPool = ThreadPoolExecutor(
|
||||
CORE_POOL_SIZE,
|
||||
MAX_POOL_SIZE,
|
||||
KEEP_ALIVE_TIME,
|
||||
TimeUnit.SECONDS,
|
||||
LinkedBlockingQueue()
|
||||
) { runnable ->
|
||||
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
|
||||
isDaemon = true
|
||||
priority = Thread.NORM_PRIORITY
|
||||
}
|
||||
}.asCoroutineDispatcher()
|
||||
|
||||
private val appListMutex = Mutex()
|
||||
|
||||
private val configChangeListeners = mutableSetOf<(String) -> Unit>()
|
||||
|
||||
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
var search by mutableStateOf("")
|
||||
var showSystemApps by mutableStateOf(false)
|
||||
|
||||
var showSystemApps by mutableStateOf(loadShowSystemApps())
|
||||
private set
|
||||
|
||||
var selectedCategory by mutableStateOf(loadSelectedCategory())
|
||||
private set
|
||||
|
||||
var currentSortType by mutableStateOf(loadCurrentSortType())
|
||||
private set
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
@@ -72,6 +152,89 @@ class SuperUserViewModel : ViewModel() {
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
internal set
|
||||
|
||||
// 加载进度状态
|
||||
var loadingProgress by mutableFloatStateOf(0f)
|
||||
private set
|
||||
var loadingMessage by mutableStateOf("")
|
||||
private set
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载显示系统应用设置
|
||||
*/
|
||||
private fun loadShowSystemApps(): Boolean {
|
||||
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载选择的应用分类
|
||||
*/
|
||||
private fun loadSelectedCategory(): AppCategory {
|
||||
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey
|
||||
return AppCategory.fromPersistKey(categoryKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载当前排序方式
|
||||
*/
|
||||
private fun loadCurrentSortType(): SortType {
|
||||
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey
|
||||
return SortType.fromPersistKey(sortKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新显示系统应用设置并保存到SharedPreferences
|
||||
*/
|
||||
fun updateShowSystemApps(newValue: Boolean) {
|
||||
showSystemApps = newValue
|
||||
saveShowSystemApps(newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选择的应用分类并保存到SharedPreferences
|
||||
*/
|
||||
fun updateSelectedCategory(newCategory: AppCategory) {
|
||||
selectedCategory = newCategory
|
||||
saveSelectedCategory(newCategory)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前排序方式并保存到SharedPreferences
|
||||
*/
|
||||
fun updateCurrentSortType(newSortType: SortType) {
|
||||
currentSortType = newSortType
|
||||
saveCurrentSortType(newSortType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存显示系统应用设置到SharedPreferences
|
||||
*/
|
||||
private fun saveShowSystemApps(value: Boolean) {
|
||||
prefs.edit {
|
||||
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
|
||||
}
|
||||
Log.d(TAG, "Saved show system apps: $value")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存选择的应用分类到SharedPreferences
|
||||
*/
|
||||
private fun saveSelectedCategory(category: AppCategory) {
|
||||
prefs.edit {
|
||||
putString(KEY_SELECTED_CATEGORY, category.persistKey)
|
||||
}
|
||||
Log.d(TAG, "Saved selected category: ${category.persistKey}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前排序方式到SharedPreferences
|
||||
*/
|
||||
private fun saveCurrentSortType(sortType: SortType) {
|
||||
prefs.edit {
|
||||
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
|
||||
}
|
||||
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
|
||||
}
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
when {
|
||||
@@ -127,6 +290,43 @@ class SuperUserViewModel : ViewModel() {
|
||||
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
updateAppProfileLocally(packageName, updatedProfile)
|
||||
notifyConfigChange(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
showBatchActions = false
|
||||
refreshAppConfigurations()
|
||||
}
|
||||
|
||||
// 批量更新权限和umount模块设置
|
||||
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
|
||||
selectedApps.forEach { packageName ->
|
||||
val app = apps.find { it.packageName == packageName }
|
||||
app?.let {
|
||||
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||
val updatedProfile = profile.copy(
|
||||
allowSu = allowSu,
|
||||
umountModules = umountModules ?: profile.umountModules,
|
||||
nonRootUseDefault = false
|
||||
)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
updateAppProfileLocally(packageName, updatedProfile)
|
||||
notifyConfigChange(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
showBatchActions = false
|
||||
refreshAppConfigurations()
|
||||
}
|
||||
|
||||
// 更新本地应用配置
|
||||
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||
appListMutex.tryLock().let { locked ->
|
||||
if (locked) {
|
||||
try {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
@@ -134,74 +334,158 @@ class SuperUserViewModel : ViewModel() {
|
||||
app
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
appListMutex.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
showBatchActions = false // 批量操作完成后退出批量模式
|
||||
fetchAppList() // 刷新列表以显示最新状态
|
||||
}
|
||||
|
||||
private suspend fun connectKsuService(
|
||||
onDisconnect: () -> Unit = {}
|
||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onDisconnect()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
continuation.resume(binder as IBinder to this)
|
||||
private fun notifyConfigChange(packageName: String) {
|
||||
configChangeListeners.forEach { listener ->
|
||||
try {
|
||||
listener(packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error notifying config change for $packageName", e)
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
|
||||
val task = KsuService.bindOrTask(
|
||||
intent,
|
||||
Shell.EXECUTOR,
|
||||
connection,
|
||||
)
|
||||
val shell = KsuCli.SHELL
|
||||
task?.let { it1 -> shell.execTask(it1) }
|
||||
}
|
||||
|
||||
private fun stopKsuService() {
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
KsuService.stop(intent)
|
||||
/**
|
||||
* 刷新应用配置状态
|
||||
*/
|
||||
suspend fun refreshAppConfigurations() {
|
||||
withContext(appProcessingThreadPool) {
|
||||
supervisorScope {
|
||||
val currentApps = apps.toList()
|
||||
val batches = currentApps.chunked(BATCH_SIZE)
|
||||
|
||||
loadingProgress = 0f
|
||||
|
||||
val updatedApps = batches.mapIndexed { batchIndex, batch ->
|
||||
async {
|
||||
val batchResult = batch.map { app ->
|
||||
try {
|
||||
val updatedProfile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
app.copy(profile = updatedProfile)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error refreshing profile for ${app.packageName}", e)
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
val progress = (batchIndex + 1).toFloat() / batches.size
|
||||
loadingProgress = progress
|
||||
|
||||
batchResult
|
||||
}
|
||||
}.awaitAll().flatten()
|
||||
|
||||
appListMutex.withLock {
|
||||
apps = updatedApps
|
||||
}
|
||||
|
||||
loadingProgress = 1f
|
||||
|
||||
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAppList() {
|
||||
isRefreshing = true
|
||||
|
||||
val result = connectKsuService {
|
||||
Log.w(TAG, "KsuService disconnected")
|
||||
}
|
||||
loadingProgress = 0f
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
||||
while (!isPlatformAlive) {
|
||||
delay(500)
|
||||
}
|
||||
} ?: return@withContext // Exit early if timeout
|
||||
val pm = ksuApp.packageManager
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
val binder = result.first
|
||||
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
|
||||
try {
|
||||
val packages = Platform.getInstalledPackagesAll {
|
||||
Log.e(TAG, "getInstalledPackagesAll:", it)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
stopKsuService()
|
||||
loadingProgress = 0.3f
|
||||
|
||||
val filteredPackages = packages.filter { it.packageName != ksuApp.packageName }
|
||||
|
||||
withContext(appProcessingThreadPool) {
|
||||
supervisorScope {
|
||||
val batches = filteredPackages.chunked(BATCH_SIZE)
|
||||
|
||||
val processedApps = batches.mapIndexed { batchIndex, batch ->
|
||||
async {
|
||||
val batchResult = batch.mapNotNull { packageInfo ->
|
||||
try {
|
||||
val appInfo = packageInfo.applicationInfo!!
|
||||
val uid = appInfo.uid
|
||||
|
||||
val labelDeferred = async {
|
||||
appInfo.loadLabel(pm).toString()
|
||||
}
|
||||
val profileDeferred = async {
|
||||
Natives.getAppProfile(packageInfo.packageName, uid)
|
||||
}
|
||||
|
||||
val label = labelDeferred.await()
|
||||
val profile = profileDeferred.await()
|
||||
|
||||
AppInfo(
|
||||
label = label,
|
||||
packageInfo = packageInfo,
|
||||
profile = profile,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Error processing app ${packageInfo.packageName}",
|
||||
e
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val progress = 0.3f + (batchIndex + 1).toFloat() / batches.size * 0.6f
|
||||
loadingProgress = progress
|
||||
|
||||
batchResult
|
||||
}
|
||||
}.awaitAll().flatten()
|
||||
|
||||
appListMutex.withLock {
|
||||
apps = processedApps
|
||||
}
|
||||
|
||||
loadingProgress = 1f
|
||||
|
||||
val elapsed = SystemClock.elapsedRealtime() - start
|
||||
Log.i(TAG, "Loaded ${processedApps.size} apps in ${elapsed}ms using concurrent processing")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching app list", e)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
loadingProgress = 0f
|
||||
loadingMessage = ""
|
||||
}
|
||||
|
||||
val packages = allPackages.list
|
||||
|
||||
apps = packages.map {
|
||||
val appInfo = it.applicationInfo
|
||||
val uid = appInfo!!.uid
|
||||
val profile = Natives.getAppProfile(it.packageName, uid)
|
||||
AppInfo(
|
||||
label = appInfo.loadLabel(pm).toString(),
|
||||
packageInfo = it,
|
||||
profile = profile,
|
||||
)
|
||||
}.filter { it.packageName != ksuApp.packageName }
|
||||
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}")
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
try {
|
||||
appProcessingThreadPool.close()
|
||||
configChangeListeners.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error cleaning up resources", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.content.ServiceConnection
|
||||
import android.util.Log
|
||||
import android.content.pm.PackageInfo
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.model.IProvider
|
||||
import com.dergoogler.mmrl.platform.model.PlatformIntent
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class KsuLibSuProvider : IProvider {
|
||||
override val name = "KsuLibSu"
|
||||
|
||||
override fun isAvailable() = true
|
||||
|
||||
override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
|
||||
|
||||
private val serviceIntent
|
||||
get() = PlatformIntent(
|
||||
ksuApp,
|
||||
Platform.KsuNext,
|
||||
SuService::class.java
|
||||
)
|
||||
|
||||
override fun bind(connection: ServiceConnection) {
|
||||
RootService.bind(serviceIntent.intent, connection)
|
||||
}
|
||||
|
||||
override fun unbind(connection: ServiceConnection) {
|
||||
RootService.stop(serviceIntent.intent)
|
||||
}
|
||||
}
|
||||
|
||||
// webui x
|
||||
suspend fun initPlatform() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val active = Platform.init {
|
||||
this.context = ksuApp
|
||||
this.platform = Platform.KsuNext
|
||||
this.provider = from(KsuLibSuProvider())
|
||||
}
|
||||
|
||||
while (!active) {
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
return@withContext active
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuLibSu", "Failed to initialize platform", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
|
||||
try {
|
||||
val packages = mutableListOf<PackageInfo>()
|
||||
val userInfos = userManager.getUsers()
|
||||
|
||||
for (userInfo in userInfos) {
|
||||
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
|
||||
}
|
||||
|
||||
packages
|
||||
} catch (e: Exception) {
|
||||
catch(e)
|
||||
packageManager.getInstalledPackages(0, userManager.myUserId)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.dergoogler.mmrl.platform.model.PlatformIntent.Companion.getPlatform
|
||||
import com.dergoogler.mmrl.platform.service.ServiceManager
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
|
||||
class SuService : RootService() {
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
val mode = intent.getPlatform()
|
||||
return ServiceManager(mode)
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,11 @@ import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import com.dergoogler.mmrl.platform.model.ModId
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import java.io.File
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class WebUIActivity : ComponentActivity() {
|
||||
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||
} else {
|
||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
val taskDescription =
|
||||
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
||||
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
|
||||
addJavascriptInterface(webviewInterface, "ksu")
|
||||
setWebViewClient(webViewClient)
|
||||
loadUrl("https://mui.kernelsu.org/index.html")
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.model.ModId
|
||||
import com.dergoogler.mmrl.ui.component.Loading
|
||||
import com.dergoogler.mmrl.webui.screen.WebUIScreen
|
||||
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.ui.theme.KernelSUTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WebUIXActivity : ComponentActivity() {
|
||||
private lateinit var webView: WebView
|
||||
|
||||
private val userAgent
|
||||
get(): String {
|
||||
val ksuVersion = BuildConfig.VERSION_CODE
|
||||
|
||||
val platform = Platform.get("Unknown") {
|
||||
platform.name
|
||||
}
|
||||
|
||||
val platformVersion = Platform.get(-1) {
|
||||
moduleManager.versionCode
|
||||
}
|
||||
|
||||
val osVersion = Build.VERSION.RELEASE
|
||||
val deviceModel = Build.MODEL
|
||||
|
||||
return "SukiSU /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
webView = WebView(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
initPlatform()
|
||||
}
|
||||
|
||||
val moduleId = intent.getStringExtra("id")!!
|
||||
val name = intent.getStringExtra("name")!!
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||
} else {
|
||||
val taskDescription =
|
||||
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Platform.isAlive) {
|
||||
while (!Platform.isAlive) {
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
Loading()
|
||||
return@KernelSUTheme
|
||||
}
|
||||
|
||||
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
|
||||
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
|
||||
val dark = isSystemInDarkTheme()
|
||||
|
||||
val options = rememberWebUIOptions(
|
||||
modId = ModId(moduleId),
|
||||
debug = webDebugging,
|
||||
appVersionCode = BuildConfig.VERSION_CODE,
|
||||
isDarkMode = dark,
|
||||
enableEruda = erudaInject,
|
||||
cls = WebUIXActivity::class.java,
|
||||
userAgentString = userAgent
|
||||
)
|
||||
|
||||
WebUIScreen(
|
||||
webView = webView,
|
||||
options = options,
|
||||
interfaces = listOf(
|
||||
WebViewInterface.factory()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.view.Window
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXInterface
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
@@ -19,16 +20,20 @@ import com.sukisu.ultra.ui.util.listModules
|
||||
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||
import com.sukisu.ultra.ui.util.listKpmModules
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class WebViewInterface(
|
||||
val context: Context,
|
||||
private val webView: WebView,
|
||||
private val modDir: String
|
||||
) {
|
||||
wxOptions: WXOptions,
|
||||
) : WXInterface(wxOptions) {
|
||||
override var name: String = "ksu"
|
||||
|
||||
companion object {
|
||||
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
|
||||
}
|
||||
|
||||
private val modDir get() = "/data/adb/modules/${modId.id}"
|
||||
|
||||
@JavascriptInterface
|
||||
fun exec(cmd: String): String {
|
||||
@@ -170,9 +175,9 @@ class WebViewInterface(
|
||||
if (context is Activity) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (enable) {
|
||||
hideSystemUI(context.window)
|
||||
hideSystemUI(activity.window)
|
||||
} else {
|
||||
showSystemUI(context.window)
|
||||
showSystemUI(activity.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +186,7 @@ class WebViewInterface(
|
||||
@JavascriptInterface
|
||||
fun moduleInfo(): String {
|
||||
val moduleInfos = JSONArray(listModules())
|
||||
var currentModuleInfo = JSONObject()
|
||||
val currentModuleInfo = JSONObject()
|
||||
currentModuleInfo.put("moduleDir", modDir)
|
||||
val moduleId = File(modDir).getName()
|
||||
for (i in 0 until moduleInfos.length()) {
|
||||
@@ -191,7 +196,7 @@ class WebViewInterface(
|
||||
continue
|
||||
}
|
||||
|
||||
var keys = currentInfo.keys()
|
||||
val keys = currentInfo.keys()
|
||||
for (key in keys) {
|
||||
currentModuleInfo.put(key, currentInfo.get(key))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package zako.zako.zako.zakoui.activity.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavHostController
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import zako.zako.zako.zakoui.activity.util.AppData
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
|
||||
|
||||
@SuppressLint("ContextCastToActivity")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName)
|
||||
val kpmVersion = getKpmVersionUse()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val settings by activity.settingsStateFlow.collectAsState()
|
||||
|
||||
// 检查是否隐藏红点
|
||||
val isHideOtherInfo = settings.isHideOtherInfo
|
||||
val showKpmInfo = settings.showKpmInfo
|
||||
|
||||
// 收集计数数据
|
||||
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
|
||||
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
|
||||
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
|
||||
|
||||
|
||||
NavigationBar(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && !showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
|
||||
if (!isFullFeatured && 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 as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (kpmModuleCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
) {
|
||||
Text(
|
||||
text = kpmModuleCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
} else if (destination == BottomBarDestination.SuperUser) {
|
||||
if (!isFullFeatured && 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 = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (superuserCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
) {
|
||||
Text(
|
||||
text = superuserCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
} else if (destination == BottomBarDestination.Module) {
|
||||
if (!isFullFeatured && 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 = {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (moduleCount > 0 && !isHideOtherInfo) {
|
||||
Badge(
|
||||
containerColor = MaterialTheme.colorScheme.secondary)
|
||||
{
|
||||
Text(
|
||||
text = moduleCount.toString(),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
} else {
|
||||
if (!isFullFeatured && 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),style = MaterialTheme.typography.labelMedium) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
object AnimatedBottomBar {
|
||||
@Composable
|
||||
fun AnimatedBottomBarWrapper(
|
||||
showBottomBar: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = showBottomBar,
|
||||
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.util.getKpmModuleCount
|
||||
import com.sukisu.ultra.ui.util.getKpmVersion
|
||||
import com.sukisu.ultra.ui.util.getModuleCount
|
||||
import com.sukisu.ultra.ui.util.getSuperuserCount
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object AppData {
|
||||
object DataRefreshManager {
|
||||
// 私有状态流
|
||||
private val _superuserCount = MutableStateFlow(0)
|
||||
private val _moduleCount = MutableStateFlow(0)
|
||||
private val _kpmModuleCount = MutableStateFlow(0)
|
||||
|
||||
// 公开的只读状态流
|
||||
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
|
||||
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
|
||||
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
|
||||
|
||||
/**
|
||||
* 刷新所有数据计数
|
||||
*/
|
||||
fun refreshData() {
|
||||
_superuserCount.value = getSuperuserCountUse()
|
||||
_moduleCount.value = getModuleCountUse()
|
||||
_kpmModuleCount.value = getKpmModuleCountUse()
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步刷新所有数据
|
||||
*/
|
||||
suspend fun refreshDataAsync() = withContext(Dispatchers.IO) {
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超级用户应用计数
|
||||
*/
|
||||
fun getSuperuserCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
getSuperuserCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块计数
|
||||
*/
|
||||
fun getModuleCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
getModuleCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取KPM模块计数
|
||||
*/
|
||||
fun getKpmModuleCountUse(): Int {
|
||||
return try {
|
||||
if (!rootAvailable()) return 0
|
||||
val kpmVersion = getKpmVersionUse()
|
||||
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0
|
||||
getKpmModuleCount()
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取KPM版本
|
||||
*/
|
||||
fun getKpmVersionUse(): String {
|
||||
return try {
|
||||
if (!rootAvailable()) return ""
|
||||
val version = getKpmVersion()
|
||||
if (version.isEmpty()) "" else version
|
||||
} catch (e: Exception) {
|
||||
"Error: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否具有管理员权限
|
||||
*/
|
||||
fun isManager(packageName: String): Boolean {
|
||||
return Natives.becomeManager(packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是完整功能模式
|
||||
*/
|
||||
fun isFullFeatured(packageName: String): Boolean {
|
||||
val isManager = Natives.becomeManager(packageName)
|
||||
return isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object DataRefreshUtils {
|
||||
|
||||
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
DataRefreshManager.refreshData()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startSettingsMonitorCoroutine(
|
||||
scope: LifecycleCoroutineScope,
|
||||
activity: MainActivity,
|
||||
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
|
||||
) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
settingsStateFlow.value = MainActivity.SettingsState(
|
||||
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
|
||||
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshData(scope: LifecycleCoroutineScope) {
|
||||
scope.launch {
|
||||
DataRefreshManager.refreshData()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object DisplayUtils {
|
||||
|
||||
fun applyCustomDpi(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val customDpi = prefs.getInt("app_dpi", 0)
|
||||
|
||||
if (customDpi > 0) {
|
||||
try {
|
||||
val resources = context.resources
|
||||
val metrics = resources.displayMetrics
|
||||
metrics.density = customDpi / 160f
|
||||
@Suppress("DEPRECATION")
|
||||
metrics.scaledDensity = customDpi / 160f
|
||||
metrics.densityDpi = customDpi
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleUtils {
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
fun applyLanguageSetting(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val resources = context.resources
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
context.createConfigurationContext(config)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyLocale(context: Context): Context {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
var newContext = context
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
newContext = context.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
return newContext
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
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.navigation.NavBackStackEntry
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
|
||||
object NavigationUtils {
|
||||
fun 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)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package zako.zako.zako.zakoui.activity.util
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.MainActivity
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
object ThemeUtils {
|
||||
|
||||
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
|
||||
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
settingsStateFlow.value = MainActivity.SettingsState(
|
||||
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
|
||||
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
|
||||
)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(activity.applicationContext)
|
||||
}
|
||||
|
||||
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
|
||||
activity.runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activity.contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
return contentObserver
|
||||
}
|
||||
|
||||
fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) {
|
||||
activity.contentResolver.unregisterContentObserver(observer)
|
||||
}
|
||||
|
||||
fun onActivityPause(activity: MainActivity) {
|
||||
CardConfig.save(activity.applicationContext)
|
||||
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
}
|
||||
|
||||
fun onActivityResume() {
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadThemeMode() {
|
||||
}
|
||||
|
||||
private fun loadThemeColors() {
|
||||
}
|
||||
|
||||
private fun loadDynamicColorState() {
|
||||
}
|
||||
|
||||
private fun loadCustomBackground() {
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sukisu.ultra.flash
|
||||
package zako.zako.zako.zakoui.flash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
@@ -27,6 +27,11 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
data class FlashState(
|
||||
val isFlashing: Boolean = false,
|
||||
val isCompleted: Boolean = false,
|
||||
@@ -0,0 +1,418 @@
|
||||
package zako.zako.zako.zakoui.screen
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
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.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import zako.zako.zako.zakoui.flash.HorizonKernelState
|
||||
import zako.zako.zako.zakoui.flash.HorizonKernelWorker
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import zako.zako.zako.zakoui.flash.FlashState
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
private object KernelFlashStateHolder {
|
||||
var currentState: HorizonKernelState? = null
|
||||
var currentUri: Uri? = null
|
||||
var currentSlot: String? = null
|
||||
var isFlashing = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Kernel刷写界面
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KernelFlashScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
kernelUri: Uri,
|
||||
selectedSlot: String? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollState = rememberScrollState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var logText by rememberSaveable { mutableStateOf("") }
|
||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||
val logContent = rememberSaveable { StringBuilder() }
|
||||
val horizonKernelState = remember {
|
||||
if (KernelFlashStateHolder.currentState != null &&
|
||||
KernelFlashStateHolder.currentUri == kernelUri &&
|
||||
KernelFlashStateHolder.currentSlot == selectedSlot) {
|
||||
KernelFlashStateHolder.currentState!!
|
||||
} else {
|
||||
HorizonKernelState().also {
|
||||
KernelFlashStateHolder.currentState = it
|
||||
KernelFlashStateHolder.currentUri = kernelUri
|
||||
KernelFlashStateHolder.currentSlot = selectedSlot
|
||||
KernelFlashStateHolder.isFlashing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val flashState by horizonKernelState.state.collectAsState()
|
||||
val logSavedString = stringResource(R.string.log_saved)
|
||||
|
||||
val onFlashComplete = {
|
||||
showFloatAction = true
|
||||
KernelFlashStateHolder.isFlashing = false
|
||||
}
|
||||
|
||||
// 开始刷写
|
||||
LaunchedEffect(Unit) {
|
||||
if (!KernelFlashStateHolder.isFlashing && !flashState.isCompleted && flashState.error.isEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
KernelFlashStateHolder.isFlashing = true
|
||||
val worker = HorizonKernelWorker(
|
||||
context = context,
|
||||
state = horizonKernelState,
|
||||
slot = selectedSlot
|
||||
)
|
||||
worker.uri = kernelUri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
|
||||
// 监听日志更新
|
||||
while (!flashState.isCompleted && flashState.error.isEmpty()) {
|
||||
if (flashState.logs.isNotEmpty()) {
|
||||
logText = flashState.logs.joinToString("\n")
|
||||
logContent.clear()
|
||||
logContent.append(logText)
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
|
||||
if (flashState.error.isNotEmpty()) {
|
||||
logText += "\n${flashState.error}\n"
|
||||
logContent.append("\n${flashState.error}\n")
|
||||
KernelFlashStateHolder.isFlashing = false
|
||||
} else if (flashState.isCompleted) {
|
||||
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
|
||||
logContent.append("\n${context.getString(R.string.horizon_flash_complete)}\n\n\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logText = flashState.logs.joinToString("\n")
|
||||
if (flashState.error.isNotEmpty()) {
|
||||
logText += "\n${flashState.error}\n"
|
||||
} else if (flashState.isCompleted) {
|
||||
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
|
||||
showFloatAction = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onBack: () -> Unit = {
|
||||
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||
// 清理全局状态
|
||||
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
|
||||
KernelFlashStateHolder.currentState = null
|
||||
KernelFlashStateHolder.currentUri = null
|
||||
KernelFlashStateHolder.currentSlot = null
|
||||
KernelFlashStateHolder.isFlashing = false
|
||||
}
|
||||
navigator.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
flashState = flashState,
|
||||
onBack = onBack,
|
||||
onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"KernelSU_kernel_flash_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (showFloatAction) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
reboot()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(id = R.string.reboot)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.reboot))
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
expanded = true
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) {
|
||||
FlashProgressIndicator(flashState)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
LaunchedEffect(logText) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = logText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FlashProgressIndicator(flashState: FlashState) {
|
||||
val progressColor = when {
|
||||
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
|
||||
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
|
||||
val progress = animateFloatAsState(
|
||||
targetValue = flashState.progress,
|
||||
label = "FlashProgress"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = when {
|
||||
flashState.error.isNotEmpty() -> stringResource(R.string.flash_failed)
|
||||
flashState.isCompleted -> stringResource(R.string.flash_success)
|
||||
else -> stringResource(R.string.flashing)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = progressColor
|
||||
)
|
||||
|
||||
when {
|
||||
flashState.error.isNotEmpty() -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
flashState.isCompleted -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (flashState.currentStep.isNotEmpty()) {
|
||||
Text(
|
||||
text = flashState.currentStep,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.value },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(8.dp),
|
||||
color = progressColor,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
if (flashState.error.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = flashState.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
flashState: FlashState,
|
||||
onBack: () -> Unit,
|
||||
onSave: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val statusColor = when {
|
||||
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
|
||||
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.primary
|
||||
}
|
||||
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||
colorScheme.surfaceContainerLow
|
||||
} else {
|
||||
colorScheme.background
|
||||
}
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
when {
|
||||
flashState.error.isNotEmpty() -> R.string.flash_failed
|
||||
flashState.isCompleted -> R.string.flash_success
|
||||
else -> R.string.kernel_flashing
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = statusColor
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.save_log),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user